import { emit } from '@tauri-apps/api/event'
import type {
  Album,
  ExploreTagEntry,
  Folder,
  ImageRecord,
  ImageTag,
  SortOrder,
  TaggerModel,
  TaggerModelStatus,
  TaggerRuntimeProbe,
} from '../store'
import { compareImages, createMockDb, mockExif } from './mockFixtures'
import { getMockScenario, isEmptyLibraryScenario } from './mockScenarios'

const db = createMockDb(getMockScenario())
let nextFolderId = 100
let nextAlbumId = 100
let nextTagId = 10_000
// A true first run ('new-user') has no tagger model downloaded yet, so the
// onboarding AI step offers the real model-choice + download flow.
const taggerStartsUnready = (scenario: typeof db.scenario) =>
  scenario === 'unready' || scenario === 'joytag-unready' || scenario === 'new-user'
let mockTaggerModel: TaggerModel = db.scenario === 'joytag-unready' ? 'joytag' : 'wd'
let mockTaggerReady = !taggerStartsUnready(db.scenario)
const mockTaggerThresholds: Record<TaggerModel, number> = {
  wd: 0.35,
  joytag: 0.4,
}

type AnyPayload = Record<string, any> | undefined
type Rgb = [number, number, number]

const mockPalette: Rgb[] = [
  [59, 125, 216],
  [31, 182, 166],
  [76, 175, 80],
  [242, 207, 46],
  [232, 134, 46],
  [226, 59, 59],
  [139, 92, 246],
  [236, 72, 153],
  [139, 90, 43],
  [26, 26, 26],
  [245, 245, 245],
  [154, 160, 166],
]

function isRgb(value: unknown): value is Rgb {
  return (
    Array.isArray(value) && value.length === 3 && value.every((entry) => typeof entry === 'number')
  )
}

function isTaggerModel(value: unknown): value is TaggerModel {
  return value === 'wd' || value === 'joytag'
}

function colorDistanceSq(left: Rgb, right: Rgb): number {
  return (left[0] - right[0]) ** 2 + (left[1] - right[1]) ** 2 + (left[2] - right[2]) ** 2
}

function matchesMockColor(image: ImageRecord, color: Rgb): boolean {
  const primary = mockPalette[(image.id - 1) % mockPalette.length]
  const secondary = mockPalette[(image.folder_id + image.rating) % mockPalette.length]
  return colorDistanceSq(primary, color) <= 4_900 || colorDistanceSq(secondary, color) <= 4_900
}

function params(payload: unknown): Record<string, any> {
  if (!payload || typeof payload !== 'object') return {}
  const record = payload as Record<string, any>
  return record.params && typeof record.params === 'object' ? record.params : record
}

function page<T>(items: T[], offset = 0, limit = 200) {
  return {
    images: items.slice(offset, offset + limit),
    total: items.length,
    offset,
    limit,
  }
}

function mockTaggerStatus(): TaggerModelStatus {
  const modelInfo =
    mockTaggerModel === 'joytag'
      ? {
          model_id: 'fancyfeast/joytag',
          model_name: 'JoyTag',
          local_dir: 'mock://models/joytag',
          label_file: 'top_tags.txt',
        }
      : {
          model_id: 'SmilingWolf/wd-swinv2-tagger-v3',
          model_name: 'wd-swinv2-tagger-v3',
          local_dir: 'mock://models/wd-swinv2-tagger-v3',
          label_file: 'selected_tags.csv',
        }

  return {
    model_id: modelInfo.model_id,
    model_name: modelInfo.model_name,
    local_dir: modelInfo.local_dir,
    ready: mockTaggerReady,
    missing_files: mockTaggerReady ? [] : ['model.onnx', modelInfo.label_file],
  }
}

function mockTaggerRuntimeProbe(): TaggerRuntimeProbe {
  if (!mockTaggerReady) {
    throw new Error(`${mockTaggerStatus().model_name} is missing required model files`)
  }
  return {
    ready: true,
    acceleration: 'auto',
    session: { file: 'model.onnx', inputs: ['input'], outputs: ['tags'] },
  }
}

function filterImages(input: ImageRecord[], payload: unknown): ImageRecord[] {
  const p = params(payload)
  const query = String(p.search ?? p.query ?? '')
    .trim()
    .toLowerCase()
  const color = isRgb(p.color) ? p.color : null
  return input.filter((image) => {
    if (p.folder_id !== undefined && p.folder_id !== null && image.folder_id !== p.folder_id)
      return false
    if (p.media_kind && image.media_kind !== p.media_kind) return false
    if (p.favorites_only && !image.favorite) return false
    if (p.rating_min !== undefined && p.rating_min !== null && image.rating < p.rating_min)
      return false
    if (p.embedding_failed_only && image.embedding_status !== 'failed') return false
    if (p.tagging_failed_only && !image.ai_tagger_error) return false
    if (query && !image.filename.toLowerCase().includes(query)) return false
    if (color && !matchesMockColor(image, color)) return false
    return true
  })
}

function getImages(payload: unknown) {
  const p = params(payload)
  const sort = (p.sort ?? 'date_desc') as SortOrder
  return page(
    filterImages(db.images, payload).sort(compareImages(sort)),
    Number(p.offset ?? 0),
    Number(p.limit ?? 200)
  )
}

function searchTags(payload: unknown) {
  const p = params(payload)
  const query = String(p.query ?? '')
    .trim()
    .toLowerCase()
  const matchingIds = new Set(
    Object.entries(db.tagsByImageId)
      .filter(([, tags]) => tags.some((tag) => tag.tag.toLowerCase().includes(query)))
      .map(([imageId]) => Number(imageId))
  )
  const images = filterImages(db.images, { params: { ...p, query: '', search: '' } })
    .filter((image) => matchingIds.has(image.id))
    .sort(compareImages((p.sort ?? 'date_desc') as SortOrder))
  return page(images, Number(p.offset ?? 0), Number(p.limit ?? 200))
}

function searchTagsAutocomplete(payload: unknown): ExploreTagEntry[] {
  const p = params(payload)
  const query = String(p.query ?? '')
    .trim()
    .toLowerCase()
  const limit = Number(p.limit ?? 10)
  return db.exploreTags
    .filter((entry) => !query || entry.tag.toLowerCase().includes(query))
    .sort((left, right) => right.count - left.count || left.tag.localeCompare(right.tag))
    .slice(0, limit)
}

function rebuildExploreTags(): ExploreTagEntry[] {
  const entries = new Map<
    string,
    {
      imageIds: Set<number>
      representativeImageId: number
      hasAiSource: boolean
      hasUserSource: boolean
    }
  >()
  for (const [imageIdString, imageTags] of Object.entries(db.tagsByImageId)) {
    const imageId = Number(imageIdString)
    for (const imageTag of imageTags) {
      const entry = entries.get(imageTag.tag) ?? {
        imageIds: new Set<number>(),
        representativeImageId: imageId,
        hasAiSource: false,
        hasUserSource: false,
      }
      entry.imageIds.add(imageId)
      if (imageTag.source === 'ai') entry.hasAiSource = true
      if (imageTag.source === 'user') entry.hasUserSource = true
      entries.set(imageTag.tag, entry)
    }
  }
  return [...entries.entries()]
    .map(([tag, entry]) => {
      const representative = db.images.find((image) => image.id === entry.representativeImageId)
      return {
        tag,
        count: entry.imageIds.size,
        representative_image_id: entry.representativeImageId,
        thumbnail_path: representative?.thumbnail_path ?? null,
        has_ai_source: entry.hasAiSource,
        has_user_source: entry.hasUserSource,
      }
    })
    .sort((left, right) => right.count - left.count || left.tag.localeCompare(right.tag))
}

function semanticSearch(payload: unknown): ImageRecord[] {
  const p = params(payload)
  const query = String(p.query ?? '').toLowerCase()
  const scored = filterImages(db.images, payload)
    .map((image, index) => ({
      image,
      score:
        (image.generated_caption?.toLowerCase().includes(query) ? 0 : 20) +
        (image.filename.toLowerCase().includes(query) ? 0 : 10) +
        (index % 9),
    }))
    .sort((left, right) => left.score - right.score)
    .slice(0, Number(p.limit ?? 200))
    .map((entry) => entry.image)
  return scored.length ? scored : filterImages(db.images, payload).slice(0, Number(p.limit ?? 200))
}

function albumImages(payload: unknown) {
  const p = params(payload)
  const albumId = Number(p.album_id)
  const ids = new Set(db.albumImageIds[albumId] ?? [])
  const sort = (p.sort ?? 'date_desc') as SortOrder
  return page(
    db.images.filter((image) => ids.has(image.id)).sort(compareImages(sort)),
    Number(p.offset ?? 0),
    Number(p.limit ?? 200)
  )
}

function similarImages(payload: unknown) {
  const p = params(payload)
  const source = db.images.find((image) => image.id === p.image_id)
  let images = db.images.filter(
    (image) => image.id !== p.image_id && image.embedding_status === 'ready'
  )
  if (p.album_id !== undefined && p.album_id !== null) {
    const ids = new Set(db.albumImageIds[Number(p.album_id)] ?? [])
    images = images.filter((image) => ids.has(image.id))
  } else if (p.folder_id !== undefined && p.folder_id !== null) {
    images = images.filter((image) => image.folder_id === p.folder_id)
  }
  const ordered = images.sort((left, right) => {
    const leftScore =
      Math.abs(left.rating - (source?.rating ?? 0)) + (left.folder_id === source?.folder_id ? 0 : 2)
    const rightScore =
      Math.abs(right.rating - (source?.rating ?? 0)) +
      (right.folder_id === source?.folder_id ? 0 : 2)
    return leftScore - rightScore
  })
  const offset = Number(p.offset ?? 0)
  const limit = Number(p.limit ?? 200)
  return {
    images: ordered.slice(offset, offset + limit),
    offset,
    limit,
    has_more: offset + limit < ordered.length,
  }
}

function updateImages(
  ids: number[],
  updates: { favorite?: boolean | null; rating?: number | null }
) {
  const changed: ImageRecord[] = []
  for (const image of db.images) {
    if (!ids.includes(image.id)) continue
    if (updates.favorite !== null && updates.favorite !== undefined)
      image.favorite = updates.favorite
    if (updates.rating !== null && updates.rating !== undefined) image.rating = updates.rating
    changed.push({ ...image })
  }
  return changed
}

function refreshAlbumCounts() {
  for (const album of db.albums) {
    const ids = db.albumImageIds[album.id] ?? []
    if (db.scenario !== 'extreme') album.image_count = ids.length
    album.cover_image_id = ids[0] ?? null
    album.cover_thumbnail_path =
      db.images.find((image) => image.id === ids[0])?.thumbnail_path ?? null
    album.updated_at = new Date().toISOString()
  }
}

function createAlbum(name: string): Album {
  const album: Album = {
    id: nextAlbumId++,
    name,
    cover_image_id: null,
    cover_thumbnail_path: null,
    image_count: 0,
    sort_order: db.albums.length + 1,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
  }
  db.albums.push(album)
  db.albumImageIds[album.id] = []
  return album
}

function addFolder(path: string): Folder {
  const parts = path.split(/[\\/]/).filter(Boolean)
  const name = parts[parts.length - 1] ?? 'New Folder'
  const folder: Folder = {
    id: nextFolderId++,
    path,
    name,
    image_count: 0,
    indexed_at: new Date().toISOString(),
    scan_error: null,
    sort_order: db.folders.length + 1,
  }
  db.folders.push(folder)
  return folder
}

function safeFallback(cmd: string, payload: AnyPayload): unknown {
  if (cmd.includes('app|version')) return '0.1.1-ui'
  if (cmd.includes('path|resolve_directory')) return 'C:\\Users\\phokus\\AppData\\Roaming\\Phokus'
  if (cmd.includes('path|join')) {
    const paths = payload?.paths ?? payload?.args ?? []
    return Array.isArray(paths) ? paths.join('\\') : 'C:\\Users\\phokus\\AppData\\Roaming\\Phokus'
  }
  if (cmd.includes('notification|is_permission_granted')) return false
  if (cmd.includes('notification|request_permission')) return 'denied'
  if (cmd.includes('dialog|open')) return null
  if (cmd.includes('window|is_maximized')) return false
  if (cmd.includes('window|minimize')) return null
  if (cmd.includes('window|toggle_maximize')) return null
  if (cmd.includes('window|close')) return null
  if (cmd.includes('opener|reveal_item_in_dir')) return null
  if (cmd.includes('opener|open')) return null
  if (cmd.includes('process|relaunch')) return null
  console.warn('[Phokus UI Lab] Unmocked command:', cmd, payload)
  return null
}

export async function handleMockCommand(cmd: string, payload?: unknown): Promise<unknown> {
  const p = params(payload)

  switch (cmd) {
    case 'get_folders':
      return db.folders
    case 'add_folder':
      addFolder(String(p.path ?? 'C:\\Users\\phokus\\Pictures\\NewFolder'))
      return null
    case 'add_folders':
      return (p.paths ?? []).map((path: string) => ({ status: 'added', data: addFolder(path) }))
    case 'remove_folder':
      db.folders = db.folders.filter((folder) => folder.id !== p.folderId)
      return null
    case 'rename_folder': {
      const folder = db.folders.find((entry) => entry.id === p.folderId)
      if (folder) folder.name = String(p.newName ?? folder.name)
      return null
    }
    case 'update_folder_path': {
      const folder = db.folders.find((entry) => entry.id === p.folderId)
      if (folder) folder.path = String(p.newPath ?? folder.path)
      return null
    }
    case 'reindex_folder':
    case 'reorder_folders':
      return null
    case 'list_directories':
      return {
        current: p.path ?? null,
        parent: p.path ? 'C:\\Users\\phokus' : null,
        entries: db.folders.map((folder) => ({
          name: folder.name,
          path: folder.path,
          has_children: false,
        })),
      }
    case 'get_background_job_progress':
      return db.backgroundJobs
    case 'get_images':
      return getImages(payload)
    case 'semantic_search_images':
      return semanticSearch(payload)
    case 'search_images_by_tag':
      return searchTags(payload)
    case 'search_tags_autocomplete':
      return searchTagsAutocomplete(payload)
    case 'get_images_by_ids':
      return (p.image_ids ?? [])
        .map((id: number) => db.images.find((image) => image.id === id))
        .filter(Boolean)
    case 'find_similar_images':
    case 'find_similar_by_region':
      return similarImages(payload)
    case 'get_visual_clusters':
      return isEmptyLibraryScenario(db.scenario) ? [] : db.visualClusters
    case 'get_explore_tags':
      return isEmptyLibraryScenario(db.scenario)
        ? []
        : db.scenario === 'extreme'
          ? db.exploreTags
          : db.exploreTags.slice(0, Number(p.limit ?? 180))
    case 'get_related_tags': {
      const relatedCounts = new Map<string, number>()
      for (const imageTags of Object.values(db.tagsByImageId)) {
        if (!imageTags.some((tag) => tag.tag === p.tag)) continue
        for (const tag of imageTags) {
          if (tag.tag === p.tag) continue
          relatedCounts.set(tag.tag, (relatedCounts.get(tag.tag) ?? 0) + 1)
        }
      }
      return Array.from(relatedCounts.entries())
        .map(([tag, shared_count]) => ({ tag, shared_count }))
        .sort(
          (left, right) =>
            right.shared_count - left.shared_count || left.tag.localeCompare(right.tag)
        )
        .slice(0, Number(p.limit ?? 18))
    }
    case 'suggest_image_tags':
      return ['select', 'warm light']
    case 'rename_tag':
    case 'delete_tag':
      return cmd === 'delete_tag' ? 6 : null
    case 'get_image_tags':
      return db.tagsByImageId[p.image_id] ?? []
    case 'add_user_tag': {
      const tag: ImageTag = {
        id: nextTagId++,
        image_id: p.image_id,
        tag: p.tag,
        source: 'user',
        ai_model: null,
        confidence: null,
        created_at: new Date().toISOString(),
      }
      db.tagsByImageId[p.image_id] = [...(db.tagsByImageId[p.image_id] ?? []), tag]
      return tag
    }
    case 'remove_tag':
      for (const [imageId, imageTags] of Object.entries(db.tagsByImageId)) {
        db.tagsByImageId[Number(imageId)] = imageTags.filter((tag) => tag.id !== p.tag_id)
      }
      return null
    case 'get_image_exif':
      return mockExif(Number(p.image_id))
    case 'update_image_details':
      return updateImages([Number(p.image_id)], { favorite: p.favorite, rating: p.rating })[0]
    case 'bulk_update_details':
      return updateImages(p.image_ids ?? [], { favorite: p.favorite, rating: p.rating })
    case 'bulk_add_tags':
    case 'bulk_remove_tag':
      return null
    case 'delete_images_from_disk':
      return p.image_ids ?? []
    case 'list_albums':
      refreshAlbumCounts()
      return db.albums
    case 'create_album':
      return createAlbum(String(p.name ?? 'Untitled Album'))
    case 'rename_album': {
      const album = db.albums.find((entry) => entry.id === p.album_id)
      if (album) album.name = String(p.new_name ?? album.name)
      return null
    }
    case 'delete_album':
      db.albums = db.albums.filter((album) => album.id !== p.album_id)
      delete db.albumImageIds[p.album_id]
      return null
    case 'delete_albums':
      db.albums = db.albums.filter((album) => !(p.album_ids ?? []).includes(album.id))
      for (const id of p.album_ids ?? []) delete db.albumImageIds[id]
      return null
    case 'reorder_albums':
      return null
    case 'add_images_to_album': {
      const existing = new Set(db.albumImageIds[p.album_id] ?? [])
      for (const id of p.image_ids ?? []) existing.add(id)
      db.albumImageIds[p.album_id] = [...existing]
      refreshAlbumCounts()
      return p.image_ids?.length ?? 0
    }
    case 'remove_images_from_album':
      db.albumImageIds[p.album_id] = (db.albumImageIds[p.album_id] ?? []).filter(
        (id) => !(p.image_ids ?? []).includes(id)
      )
      refreshAlbumCounts()
      return null
    case 'get_album_images':
      return albumImages(payload)
    case 'load_duplicate_scan_cache':
      return db.duplicateScannedAt
        ? { groups: db.duplicateGroups, scanned_at: db.duplicateScannedAt }
        : null
    case 'find_duplicates':
      await emit('duplicate_scan_progress', {
        phase: 'hashing',
        processed: Math.floor(db.images.length * 0.6),
        total: db.images.length,
        skipped: db.scenario === 'errors' ? 3 : 0,
      })
      db.duplicateScannedAt = Math.floor(Date.now() / 1000)
      return {
        groups: db.duplicateGroups,
        scanned_files: db.images.length,
        candidate_files: db.duplicateGroups.flatMap((group) => group.images).length,
        skipped_files: db.scenario === 'errors' ? 3 : 0,
      }
    case 'invalidate_duplicate_scan_cache':
      return null
    case 'get_caption_model_status':
      return {
        model_id: 'Salesforce/blip-image-captioning-base',
        model_name: 'BLIP Base',
        local_dir: 'mock://models/caption',
        ready: true,
        missing_files: [],
      }
    case 'get_caption_acceleration':
    case 'set_caption_acceleration':
      return p.acceleration ?? 'auto'
    case 'get_caption_detail':
    case 'set_caption_detail':
      return p.detail ?? 'paragraph'
    case 'prepare_caption_model':
    case 'delete_caption_model':
      return {
        model_id: 'Salesforce/blip-image-captioning-base',
        model_name: 'BLIP Base',
        local_dir: 'mock://models/caption',
        ready: true,
        missing_files: [],
      }
    case 'probe_caption_runtime':
      return {
        ready: true,
        acceleration: 'auto',
        detail: 'paragraph',
        tokenizer_vocab_size: 30522,
        sessions: [],
      }
    case 'probe_caption_image':
      return {
        input_shape: [1, 3, 384, 384],
        output_shape: [1, 768],
        output_values: 768,
        acceleration: 'auto',
      }
    case 'generate_caption_for_image': {
      const image = db.images.find((entry) => entry.id === p.image_id)
      if (image) image.generated_caption = `Generated in UI Lab for ${image.filename}.`
      return image
    }
    case 'queue_caption_jobs':
    case 'clear_caption_jobs':
    case 'reset_generated_captions':
    case 'queue_tagging_jobs':
    case 'clear_tagging_jobs':
      return 12
    case 'reset_ai_tags': {
      const folderIds = p.folder_ids as number[] | undefined
      const folderId = p.folder_id as number | null | undefined
      const scopedImages = db.images.filter((image) =>
        folderIds && folderIds.length > 0
          ? folderIds.includes(image.folder_id)
          : folderId != null
            ? image.folder_id === folderId
            : true
      )
      let reset = 0
      for (const image of scopedImages) {
        const tags = db.tagsByImageId[image.id] ?? []
        const aiTags = tags.filter((tag) => tag.source === 'ai')
        if (
          aiTags.length > 0 ||
          image.ai_tagged_at ||
          image.ai_tagger_error ||
          image.ai_tagger_model ||
          image.ai_rating
        ) {
          reset += 1
        }
        db.tagsByImageId[image.id] = tags.filter((tag) => tag.source !== 'ai')
        image.ai_rating = null
        image.ai_tagger_model = null
        image.ai_tagged_at = null
        image.ai_tagger_error = null
      }
      db.exploreTags = rebuildExploreTags()
      return reset
    }
    case 'get_tagger_model_status':
      return mockTaggerStatus()
    case 'get_tagger_model':
      return mockTaggerModel
    case 'set_tagger_model':
      mockTaggerModel = p.model ?? 'wd'
      mockTaggerReady = !taggerStartsUnready(db.scenario)
      return mockTaggerModel
    case 'prepare_tagger_model':
      mockTaggerReady = true
      return mockTaggerStatus()
    case 'delete_tagger_model':
      mockTaggerReady = false
      return mockTaggerStatus()
    case 'get_tagger_acceleration':
    case 'set_tagger_acceleration':
      return p.acceleration ?? 'auto'
    case 'get_tagger_threshold':
      return mockTaggerThresholds[mockTaggerModel]
    case 'set_tagger_threshold': {
      const model = isTaggerModel(p.model) ? p.model : mockTaggerModel
      mockTaggerThresholds[model] = p.threshold ?? mockTaggerThresholds[model]
      return mockTaggerThresholds[model]
    }
    case 'get_tagger_batch_size':
    case 'set_tagger_batch_size':
      return p.batch_size ?? 8
    case 'probe_tagger_runtime':
      return mockTaggerRuntimeProbe()
    case 'get_tagging_queue_scope':
      return 'all'
    case 'get_tagging_queue_folder_ids':
    case 'get_muted_folder_ids':
      return []
    case 'set_tagging_queue_scope':
    case 'set_tagging_queue_folder_ids':
    case 'set_muted_folder_ids':
    case 'set_notifications_paused':
    case 'set_worker_pauses_persist':
    case 'set_worker_paused':
      return null
    case 'get_notifications_paused':
    case 'get_worker_pauses_persist':
      return !isEmptyLibraryScenario(db.scenario)
    // Only 'new-user' greets with the onboarding tour; 'empty' is a bare
    // library that has already been through it.
    case 'get_onboarding_completed':
      return db.scenario !== 'new-user'
    case 'set_onboarding_completed':
    case 'set_last_seen_version':
      return null
    case 'get_worker_states':
      return (p.folderIds ?? []).map((folder_id: number) => ({
        folder_id,
        thumbnail_paused: false,
        metadata_paused: false,
        embedding_paused: false,
        tagging_paused: false,
      }))
    case 'get_build_variant':
      return 'cpu'
    // 'just-updated' reports an older last-seen version, so the real
    // initWhatsNew logic fires the post-update "What's new" toast on launch.
    case 'get_last_seen_version':
      return db.scenario === 'just-updated' ? '0.1.0' : '0.1.1-ui'
    case 'get_ffmpeg_status':
      return { installed: true, downloading: false, failed: false }
    case 'retry_ffmpeg_download':
    case 'open_app_data_folder':
    case 'open_map_location':
    case 'open_changelog_url':
      return null
    case 'get_database_info':
      return { size_mb: 14.2, reclaimable_mb: 1.8 }
    case 'vacuum_database':
      return { before_mb: 14.2, after_mb: 12.4, freed_mb: 1.8 }
    case 'rebuild_semantic_index':
      return db.images.length
    case 'get_orphaned_thumbnails_info':
      return { count: 9, size_mb: 3.4 }
    case 'cleanup_orphaned_thumbnails':
      return { deleted_count: 9, freed_mb: 3.4 }
    case 'retry_failed_embeddings':
      return null
    default:
      return safeFallback(cmd, payload as AnyPayload)
  }
}
