import axios from 'axios'

// Relative API base. In production the nginx in front of the SPA proxies
// /api/ to the backend container; in dev the vite server has the same
// proxy in vite.config.ts. Using a relative URL means requests are
// always same-origin, so the app works whether you hit it from
// localhost, a LAN IP, or a reverse proxy without any CORS dance.
const API_BASE_URL = '/api/v1'

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
})

// ── Auth interceptors ──────────────────────────────────────────────────

// Attach the stored JWT to every outgoing request.
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// On 401 responses, attempt one silent token refresh. If that also
// fails, clear stored credentials so the AuthContext falls back to the
// login screen on its next render.
let isRefreshing = false
let refreshSubscribers: ((token: string) => void)[] = []

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const original = error.config
    if (error.response?.status !== 401 || original._retry) {
      return Promise.reject(error)
    }

    // Skip retry for auth endpoints themselves to avoid loops.
    if (original.url?.startsWith('/auth/')) {
      return Promise.reject(error)
    }

    original._retry = true

    if (!isRefreshing) {
      isRefreshing = true
      // The refresh token lives in AuthContext memory, not in
      // localStorage. The interceptor can't access it directly, so we
      // rely on the AuthContext's scheduled refresh to keep the access
      // token fresh. If the access token is truly expired and no
      // refresh has happened, we just force a logout.
      localStorage.removeItem('access_token')
      isRefreshing = false
      // Reject — AuthContext will detect the missing token and show login.
      return Promise.reject(error)
    }

    // Another request is already refreshing — queue this one.
    return new Promise((resolve) => {
      refreshSubscribers.push((token: string) => {
        original.headers.Authorization = `Bearer ${token}`
        resolve(api(original))
      })
    })
  },
)

// Source Folders API. Source roots are config-driven now (PHOTO_DIRS in
// .env → bootstrap on backend startup), so the UI only reads them and
// optionally renames the display label.
export interface FolderTreeNode {
  id: string
  name: string
  path: string
  photo_count: number
  /** User-set "hide from cross-cutting views" flag. When true, photos
   *  in this folder (and descendants) are excluded from All Photos,
   *  Map, Tags, People, Search and sidebar counts, but remain visible
   *  when the user navigates directly into the folder. */
  is_hidden: boolean
  children: FolderTreeNode[]
}

export const sourceFolders = {
  list: async () => {
    const response = await api.get('/folders')
    return response.data
  },

  /** Recursive folder tree, one root per active source root. */
  tree: async (): Promise<FolderTreeNode[]> => {
    const response = await api.get('/folders/tree')
    return response.data
  },

  scan: async (folderId: string) => {
    const response = await api.post(`/folders/${folderId}/scan`)
    return response.data
  },

  /** Rename a folder. SourceRoot ids only update the display label;
   *  Folder ids actually move the directory on disk and update every
   *  descendant photo's filepath. */
  rename: async (folderId: string, name: string) => {
    const response = await api.patch(`/folders/${folderId}`, { name })
    return response.data
  },

  /** Create a new sub-folder under an existing Folder. parent_id MUST
   *  be a Folder row id (not a SourceRoot id). */
  create: async (parentId: string, name: string) => {
    const response = await api.post('/folders', {
      name,
      parent_id: parentId,
    })
    return response.data as { id: string; name: string; path: string; parent_id: string }
  },

  /** Toggle "hide from views" on a folder or source root. Hidden
   *  folders still index + thumbnail their photos, but those photos
   *  are excluded from cross-cutting views (All Photos, Map, Tags,
   *  People, Search, sidebar counts). Navigating directly into the
   *  folder still shows them. Propagates to every descendant folder. */
  setHidden: async (folderId: string, hidden: boolean) => {
    const response = await api.post(`/folders/${folderId}/hide`, {
      hidden,
    })
    return response.data as {
      id: string
      name: string
      path: string
      is_hidden: boolean
    }
  },

  /** Delete a folder. mode=discard moves all photos under it to the
   *  discard pile (recoverable) and leaves the folder + on-disk dir
   *  alone. mode=permanent unlinks files, removes folder rows, and
   *  rmtrees the directory — irreversible. */
  delete: async (folderId: string, mode: 'discard' | 'permanent') => {
    const response = await api.delete(`/folders/${folderId}`, {
      params: { mode },
    })
    return response.data as {
      status: string
      mode: string
      discarded?: number
      deleted_photos?: number
      file_errors?: number
    }
  },
}

// Photos API
export const photos = {
  list: async (params?: {
    limit?: number
    offset?: number
    folder_id?: string
    heap_id?: string
    rating?: number
    flag?: string
  }) => {
    const response = await api.get('/photos', { params })
    return response.data
  },

  get: async (photoId: string) => {
    const response = await api.get(`/photos/${photoId}`)
    return response.data
  },

  /** Lightweight list of every photo with GPS coordinates, used by the
   *  Map view. Returns one tiny object per photo (id, lat, lon, taken_at)
   *  rather than the full photo payload — keeps responses small even on
   *  big libraries. */
  mapPoints: async () => {
    const response = await api.get('/photos/map')
    return response.data as Array<{
      id: string
      latitude: number
      longitude: number
      taken_at: string | null
    }>
  },

  update: async (photoId: string, data: {
    filename?: string
    rating?: number
    user_title?: string | null
    user_notes?: string | null
    color_label?: string | null
    is_picked?: boolean
    is_discarded?: boolean
    taken_at?: string
  }) => {
    const response = await api.patch(`/photos/${photoId}`, data)
    return response.data
  },

  /** Bulk discard — matches the backend BulkAction schema. */
  bulkDiscard: async (photoIds: string[]) => {
    const response = await api.post('/photos/bulk', {
      ids: photoIds,
      action: 'discard',
    })
    return response.data
  },

  /** Bulk restore from discarded. */
  bulkRestore: async (photoIds: string[]) => {
    const response = await api.post('/photos/bulk', {
      ids: photoIds,
      action: 'restore',
    })
    return response.data
  },

  /** Bulk set rating (0-5). */
  bulkSetRating: async (photoIds: string[], rating: number) => {
    const response = await api.post('/photos/bulk', {
      ids: photoIds,
      action: 'set_rating',
      value: rating,
    })
    return response.data
  },

  /** Bulk set color label (or null to clear). */
  bulkSetColor: async (photoIds: string[], color: string | null) => {
    const response = await api.post('/photos/bulk', {
      ids: photoIds,
      action: 'set_color',
      value: color,
    })
    return response.data
  },

  /** Bulk set taken_at — one ISO datetime applied to every listed photo.
   *  Backend rewrites EXIF on disk per-photo and surfaces per-photo errors
   *  in the `errors` array so the UI can report a partial apply. */
  bulkSetTakenAt: async (photoIds: string[], isoDatetime: string) => {
    const response = await api.post('/photos/bulk', {
      ids: photoIds,
      action: 'set_taken_at',
      value: isoDatetime,
    })
    return response.data as {
      status: string
      updated: number
      skipped: number
      errors: { id: string; message: string }[]
    }
  },

  /** Bulk set taken_at with a per-photo map. Used by the "guess from folder"
   *  flow where every selected photo gets its own suggested date. */
  bulkSetTakenAtMap: async (map: Record<string, string>) => {
    const response = await api.post('/photos/bulk', {
      ids: Object.keys(map),
      action: 'set_taken_at_map',
      value: map,
    })
    return response.data as {
      status: string
      updated: number
      skipped: number
      errors: { id: string; message: string }[]
    }
  },

  /** Add the listed tags to every listed photo. Idempotent — re-adding
   *  an existing (photo, tag) pair is a no-op. Returns { added: N }. */
  bulkAddTags: async (photoIds: string[], tagIds: string[]) => {
    const response = await api.post('/photos/bulk', {
      ids: photoIds,
      action: 'add_tags',
      value: tagIds,
    })
    return response.data as { status: string; added: number }
  },

  /** Remove the listed tags from every listed photo. Removing a
   *  non-member is a no-op. Returns { removed: N }. */
  bulkRemoveTags: async (photoIds: string[], tagIds: string[]) => {
    const response = await api.post('/photos/bulk', {
      ids: photoIds,
      action: 'remove_tags',
      value: tagIds,
    })
    return response.data as { status: string; removed: number }
  },

  /** Move photos into a target folder (or source root). Returns
   *  { moved, errors[] }. */
  move: async (photoIds: string[], targetId: string) => {
    const response = await api.post('/photos/move', {
      photo_ids: photoIds,
      target_id: targetId,
    })
    return response.data as { status: string; moved: number; errors: Array<{ id: string; error: string }> }
  },

  /** Copy photos into a target folder. Originals are unaffected; new
   *  rows are created with is_duplicate=true. */
  copy: async (photoIds: string[], targetId: string) => {
    const response = await api.post('/photos/copy', {
      photo_ids: photoIds,
      target_id: targetId,
    })
    return response.data as { status: string; copied: number; errors: Array<{ id: string; error: string }> }
  },

  getThumbnailUrl: (photoId: string, size: 'small' | 'medium' | 'large' = 'medium') => {
    const token = localStorage.getItem('access_token')
    const qs = token ? `?token=${encodeURIComponent(token)}` : ''
    return `${API_BASE_URL}/photos/${photoId}/thumb/${size}${qs}`
  },

  getOriginalUrl: (photoId: string) => {
    const token = localStorage.getItem('access_token')
    const qs = token ? `?token=${encodeURIComponent(token)}` : ''
    return `${API_BASE_URL}/photos/${photoId}/original${qs}`
  },

  /** Full-resolution display URL. Backend serves the original for web-safe
   *  formats and a transcoded WebP for RAW/HEIC/TIFF. */
  getProxyUrl: (photoId: string) => {
    const token = localStorage.getItem('access_token')
    const qs = token ? `?token=${encodeURIComponent(token)}` : ''
    return `${API_BASE_URL}/photos/${photoId}/proxy${qs}`
  },

  /** "On this day" memories — photos taken on this date in previous years. */
  memories: async (): Promise<MemoriesResponse> => {
    const response = await api.get('/photos/memories')
    return response.data
  },
}

// Library API
export type MediaType = 'photo' | 'raw' | 'heic' | 'video'

export interface ThumbnailStats {
  total: number
  pending: number
  processing: number
  completed: number
  failed: number
  by_media_type: Record<string, number>
}

export interface RegenerateResult {
  status: string
  matched: number
  queued: number
  cleared_dirs: number
  file_errors: number
  filters: {
    media_types: MediaType[] | null
    only_failed: boolean
  }
}

export interface MissingStats {
  would_delete?: number
  deleted?: number
  would_delete_folders?: number
  deleted_folders?: number
  skipped_unmounted: number
  dry_run: boolean
}

export interface PruneResult extends MissingStats {
  status?: string
  message?: string
}

export interface WorkerInfo {
  name: string
  status: 'online' | 'unreachable'
  active: number
  reserved: number
  scheduled: number
  concurrency: number | null
  processed: Record<string, number>
  queues: string[]
  active_tasks: Array<{
    id: string
    name: string
    args: unknown
    time_start: number | null
  }>
}

export interface WorkerFailure {
  photo_id: string
  filename: string
  media_type: string
  error: string
  updated_at: string | null
}

export interface WorkerStatus {
  broker_ok: boolean
  broker_error: string | null
  inspect_error: string | null
  workers: WorkerInfo[]
  worker_count: number
  queues: Record<string, number>
  failures: {
    total: number
    recent: WorkerFailure[]
  }
  scan_errors: string[]
}

export interface PipelineStage {
  key: string
  label: string
  done: number
  total: number
  hint: string
  /** True when the stage legitimately runs on a subset of photos — e.g.
   *  GPS / tags / OCR / faces — so 100% coverage is never expected and
   *  the UI should not frame "missing" as a problem. */
  partial?: boolean
  /** True when the stage doesn't have a done/total progress semantic
   *  (e.g. face clusters, duplicate groups — those are output counts,
   *  not ratios). The UI renders a plain count instead of a bar. */
  standalone?: boolean
}

export interface PipelineStats {
  total_photos: number
  total_images: number
  stages: PipelineStage[]
}

export interface ScanStatus {
  is_scanning: boolean
  current_folder: string | null
  processed_files: number
  total_files: number
  errors: string[]
}

export const library = {
  scan: async () => {
    const response = await api.post('/library/scan')
    return response.data
  },

  scanStatus: async () => {
    const response = await api.get('/library/scan/status')
    return response.data
  },

  stats: async (scope?: 'global'): Promise<LibraryStats> => {
    const response = await api.get('/library/stats', {
      params: scope ? { scope } : undefined,
    })
    return response.data
  },

  /** Maintenance / admin actions surfaced via the Settings panel. */
  maintenance: {
    thumbnailStats: async (scope?: 'global'): Promise<ThumbnailStats> => {
      const response = await api.get('/library/maintenance/thumbnail-stats', {
        params: scope ? { scope } : undefined,
      })
      return response.data
    },

    /** Reset on-disk thumbs and re-queue Celery generation. With no
     *  filters, every photo in the library is re-queued. */
    regenerateThumbnails: async (
      body: {
        media_types?: MediaType[]
        only_failed?: boolean
        only_pending?: boolean
      } = {},
      scope?: 'global',
    ): Promise<RegenerateResult> => {
      const response = await api.post(
        '/library/maintenance/regenerate-thumbnails',
        body,
        { params: scope ? { scope } : undefined },
      )
      return response.data
    },

    /** Celery worker fleet diagnostics + recent task failures. */
    workerStatus: async (scope?: 'global'): Promise<WorkerStatus> => {
      const response = await api.get('/library/maintenance/worker-status', {
        params: scope ? { scope } : undefined,
      })
      return response.data
    },

    /** Per-stage ingestion progress. */
    pipelineStats: async (scope?: 'global'): Promise<PipelineStats> => {
      const response = await api.get('/library/maintenance/pipeline-stats', {
        params: scope ? { scope } : undefined,
      })
      return response.data
    },

    /** Dry-run count of photo rows whose files are no longer on disk
     *  (under a mounted source root). */
    missingStats: async (): Promise<MissingStats> => {
      const response = await api.get('/library/maintenance/missing-stats')
      return response.data
    },

    /** Actually delete the orphaned photo rows. */
    pruneMissing: async (): Promise<PruneResult> => {
      const response = await api.post('/library/maintenance/prune-missing')
      return response.data
    },

    /** Re-run the source-roots / folders / photos integrity cleanup that
     *  normally runs on backend startup. */
    cleanup: async (): Promise<{ status: string; message?: string }> => {
      const response = await api.post('/library/maintenance/cleanup')
      return response.data
    },

    /** Recompute duplicate groups from current perceptual hashes.
     *  Idempotent — safe to fire repeatedly. */
    regroupDuplicates: async (): Promise<{ status: string; message?: string }> => {
      const response = await api.post('/library/maintenance/regroup-duplicates')
      return response.data
    },

    /** Compute pHash for every photo currently missing one. One-shot
     *  recovery path for libraries that existed before the phash column
     *  was added. */
    backfillPhashes: async (): Promise<{ status: string; message?: string }> => {
      const response = await api.post('/library/maintenance/backfill-phashes')
      return response.data
    },
  },

  /** Duplicate groups computed by app.services.duplicates.regroup_duplicates.
   *  Drives the grouped grid view in the Duplicates section. */
  duplicates: {
    groups: async (scope?: 'global'): Promise<DuplicateGroupsResponse> => {
      const response = await api.get('/library/duplicates/groups', {
        params: scope ? { scope } : undefined,
      })
      return response.data
    },
  },
}

// ── Duplicate groups ─────────────────────────────────────────────────────

export interface DuplicateGroupMember {
  id: string
  filename: string
  taken_at: string | null
  file_size: number | null
  width: number | null
  height: number | null
  thumb_small: string | null
  file_hash: string | null
  folder_id: string | null
  media_type: string
}

export interface DuplicateGroup {
  group_id: string
  member_count: number
  /** "exact" iff every member shares the same SHA-256 (true byte
   *  duplicates that pHash also caught). "similar" otherwise. */
  reason: 'exact' | 'similar'
  members: DuplicateGroupMember[]
}

export interface DuplicateGroupsResponse {
  groups: DuplicateGroup[]
  total_groups: number
  total_members: number
}

// ── Memories ("On this day") ────────────────────────────────────────────

export interface MemoryPhoto {
  id: string
  filename: string
  taken_at: string
  thumb_small: string | null
  thumb_medium: string | null
  media_type: string
  width: number | null
  height: number | null
  rating: number
}

export interface MemoryGroup {
  year: number
  years_ago: number
  photos: MemoryPhoto[]
}

export interface MemoriesResponse {
  date: string
  memories: MemoryGroup[]
}

export interface LibraryStats {
  all_photos: number
  rated: number
  colored: number
  with_gps: number
  duplicates: number
  discarded: number
  needs_review: number
  total_photos: number
  total_videos: number
  total_size: number
  total_size_gb: number
  source_dirs: string[]
}

// Heaps API
export interface Heap {
  id: string
  name: string
  is_active: boolean
  created_at: string
  updated_at: string | null
  photo_count: number
}

export const heaps = {
  list: async (): Promise<Heap[]> => {
    const response = await api.get('/heaps')
    return response.data
  },

  create: async (name: string): Promise<Heap> => {
    const response = await api.post('/heaps', { name })
    return response.data
  },

  update: async (
    heapId: string,
    data: { name?: string; is_active?: boolean }
  ): Promise<Heap> => {
    const response = await api.patch(`/heaps/${heapId}`, data)
    return response.data
  },

  delete: async (heapId: string): Promise<void> => {
    await api.delete(`/heaps/${heapId}`)
  },

  /** Duplicate a heap, copying its membership but never marking the new
   *  one as active. The new heap is named "{name} (copy)". */
  duplicate: async (heapId: string): Promise<Heap> => {
    const response = await api.post(`/heaps/${heapId}/duplicate`)
    return response.data
  },

  /** Lightweight: just the photo ids in a heap, for client-side membership
   *  lookups (the basket affordance on thumbnails). */
  photoIds: async (heapId: string): Promise<string[]> => {
    const response = await api.get(`/heaps/${heapId}/photo_ids`)
    return response.data
  },

  addPhotos: async (heapId: string, photoIds: string[]) => {
    const response = await api.post(`/heaps/${heapId}/photos`, {
      photo_ids: photoIds,
    })
    return response.data
  },

  removePhotos: async (heapId: string, photoIds: string[]) => {
    const response = await api.delete(`/heaps/${heapId}/photos`, {
      data: { photo_ids: photoIds },
    })
    return response.data
  },

  /** Convert a heap into a folder by moving (or copying) every member
   *  photo into the target directory. Optionally creates a subfolder
   *  inside the target by name. */
  convert: async (
    heapId: string,
    body: {
      target_id: string
      mode: 'move' | 'copy'
      delete_heap: boolean
      subfolder_name?: string | null
    }
  ) => {
    const response = await api.post(`/heaps/${heapId}/convert`, body)
    return response.data as {
      status: string
      mode: 'move' | 'copy'
      moved: number
      copied: number
      errors: Array<{ id: string; error: string }>
      heap_deleted: boolean
    }
  },
}

// Upload API — single file per request so the browser can fan out many
// POSTs in parallel with per-file progress. For folder uploads the
// caller passes each File's webkitRelativePath so the backend can
// materialise the folder structure under the destination.
export const uploads = {
  uploadFile: async (
    file: File,
    destinationFolderId: string,
    opts: {
      relativePath?: string
      heapId?: string | null
      onProgress?: (loadedBytes: number, totalBytes: number) => void
      signal?: AbortSignal
    } = {}
  ) => {
    const form = new FormData()
    form.append('file', file)
    form.append('destination_folder_id', destinationFolderId)
    if (opts.relativePath) form.append('relative_path', opts.relativePath)
    if (opts.heapId) form.append('heap_id', opts.heapId)

    const response = await api.post('/upload', form, {
      headers: { 'Content-Type': 'multipart/form-data' },
      signal: opts.signal,
      onUploadProgress: (evt) => {
        if (opts.onProgress && evt.total) opts.onProgress(evt.loaded, evt.total)
      },
    })
    return response.data as {
      photo_id: string
      filename: string
      folder_id: string
      folder_path: string
      heap_id: string | null
    }
  },
}

// Download helpers — build a URL the browser can pull directly via an
// <a href>. The backend accepts `?token=` so an <a> works without a
// custom fetch + save-blob dance; the Authorization header is not
// settable on a plain link click.
export const downloads = {
  folderUrl: (folderId: string): string => {
    const token = localStorage.getItem('access_token') || ''
    return `${API_BASE_URL}/download/folders/${folderId}?token=${encodeURIComponent(token)}`
  },
  heapUrl: (heapId: string): string => {
    const token = localStorage.getItem('access_token') || ''
    return `${API_BASE_URL}/download/heaps/${heapId}?token=${encodeURIComponent(token)}`
  },
  trigger: (url: string) => {
    // Kicking off a download via a transient <a> click keeps the
    // browser in charge of the file dialog + progress indicator. We
    // use target=_blank so the current SPA route isn't replaced if
    // the server returns an error mid-stream.
    const a = document.createElement('a')
    a.href = url
    a.rel = 'noopener'
    a.target = '_blank'
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
  },
}

// Sharing API

export interface SharedHeap {
  id: string
  name: string
  owner_username: string
  permission: 'read' | 'write'
  photo_count: number
}

export interface SharedFolder {
  id: string
  name: string
  folder_type: 'folder' | 'source_root'
  owner_username: string
  permission: 'read' | 'write'
  photo_count: number
}

export interface ShareInfo {
  id: string
  shared_with_id: string
  shared_with_username: string
  permission: string
  created_at: string
}

export const sharing = {
  // Heap shares
  shareHeap: async (heapId: string, username: string, permission: 'read' | 'write' = 'read') => {
    const response = await api.post(`/sharing/heaps/${heapId}`, { username, permission })
    return response.data
  },
  heapShares: async (heapId: string): Promise<ShareInfo[]> => {
    const response = await api.get(`/sharing/heaps/${heapId}`)
    return response.data
  },
  revokeHeapShare: async (heapId: string, shareId: string) => {
    await api.delete(`/sharing/heaps/${heapId}/${shareId}`)
  },
  sharedHeaps: async (): Promise<SharedHeap[]> => {
    const response = await api.get('/sharing/heaps/shared-with-me')
    return response.data
  },

  // Folder shares
  shareFolder: async (folderId: string, username: string, permission: 'read' | 'write' = 'read') => {
    const response = await api.post(`/sharing/folders/${folderId}`, { username, permission })
    return response.data
  },
  folderShares: async (folderId: string): Promise<ShareInfo[]> => {
    const response = await api.get(`/sharing/folders/${folderId}`)
    return response.data
  },
  revokeFolderShare: async (folderId: string, shareId: string) => {
    await api.delete(`/sharing/folders/${folderId}/${shareId}`)
  },
  sharedFolders: async (): Promise<SharedFolder[]> => {
    const response = await api.get('/sharing/folders/shared-with-me')
    return response.data
  },
}

// Tags API
export type TagKind = 'user' | 'content_type'

export interface Tag {
  id: string
  name: string
  color: string | null
  kind: TagKind
  source: string | null
  representative_photo_id: string | null
  photo_count: number
}

export const tags = {
  list: async (kind?: TagKind): Promise<Tag[]> => {
    const params = kind ? { kind } : undefined
    const response = await api.get('/tags', { params })
    return response.data
  },

  create: async (name: string, color?: string, kind: TagKind = 'user'): Promise<Tag> => {
    const response = await api.post('/tags', { name, color, kind })
    return response.data
  },

  update: async (tagId: string, data: { name?: string; color?: string }): Promise<Tag> => {
    const response = await api.patch(`/tags/${tagId}`, data)
    return response.data
  },

  delete: async (tagId: string): Promise<void> => {
    await api.delete(`/tags/${tagId}`)
  },

  /** Add one or more tags to a photo. */
  addToPhoto: async (photoId: string, tagIds: string[]) => {
    const response = await api.post(`/photos/${photoId}/tags`, { tag_ids: tagIds })
    return response.data
  },

  /** Remove a tag from a photo. */
  removeFromPhoto: async (photoId: string, tagId: string): Promise<void> => {
    await api.delete(`/photos/${photoId}/tags/${tagId}`)
  },
}

// Search API — hybrid FTS + semantic search
export interface SearchResult {
  id: string
  filename: string
  filepath: string
  media_type: string
  width: number
  height: number
  taken_at: string | null
  rating: number
  color_label: string | null
  thumb_small: string
  thumb_medium: string
  score: number
}

export const search = {
  query: async (params: {
    q?: string
    filters?: {
      tag_ids?: string[]
      date_from?: string
      date_to?: string
    }
    limit?: number
    offset?: number
  }): Promise<{ results: SearchResult[]; total: number }> => {
    const response = await api.post('/photos/search', params)
    return response.data
  },
}

// Discard API
export const discard = {
  list: async () => {
    const response = await api.get('/discard')
    return response.data
  },

  restore: async (photoIds: string[]) => {
    const response = await api.post('/discard/restore', {
      photo_ids: photoIds,
    })
    return response.data
  },

  empty: async () => {
    const response = await api.delete('/discard/empty')
    return response.data
  },

  /** Permanently delete a specific subset of discarded photos. The backend
   *  silently skips ids that aren't in the pile, so this can never bypass
   *  the soft-delete safety net. */
  deletePermanent: async (photoIds: string[]) => {
    const response = await api.delete('/discard', {
      data: { photo_ids: photoIds },
    })
    return response.data
  },
}

// Admin API — user management (admin only)
export interface AdminUser {
  id: string
  username: string
  email: string | null
  role: 'admin' | 'user'
  is_active: boolean
  media_path: string
  created_at: string | null
  photo_count: number
}

export const admin = {
  listUsers: async (): Promise<{ users: AdminUser[]; total: number }> => {
    const response = await api.get('/admin/users')
    return response.data
  },

  createUser: async (data: {
    username: string
    password: string
    role: string
  }): Promise<AdminUser> => {
    const response = await api.post('/admin/users', data)
    return response.data
  },

  updateUser: async (
    userId: string,
    data: { role?: string; is_active?: boolean; new_password?: string },
  ): Promise<AdminUser> => {
    const response = await api.patch(`/admin/users/${userId}`, data)
    return response.data
  },

  deleteUser: async (userId: string): Promise<{ status: string }> => {
    const response = await api.delete(`/admin/users/${userId}`)
    return response.data
  },

  // --- AI / vision feature flags + manual pipeline triggers -----------

  listFeatureFlags: async (): Promise<{ flags: FeatureFlagSnapshot }> => {
    const response = await api.get('/admin/feature-flags')
    return response.data
  },

  /** Set ``value`` to toggle; pass ``null`` to clear the override and
   *  fall back to the YAML default. */
  setFeatureFlag: async (
    name: string,
    value: boolean | null,
  ): Promise<{ flags: FeatureFlagSnapshot }> => {
    const response = await api.patch(`/admin/feature-flags/${encodeURIComponent(name)}`, { value })
    return response.data
  },

  triggerAiBackfill: async (body: {
    limit?: number | null
  }): Promise<{ status: string; task_id: string }> => {
    const response = await api.post('/admin/ai/backfill', body)
    return response.data
  },

  triggerFullRescan: async (): Promise<{ status: string; task_id: string }> => {
    const response = await api.post('/admin/ai/rescan')
    return response.data
  },
}

export interface FeatureFlagState {
  effective: boolean
  default: boolean
  overridden: boolean
}

export type FeatureFlagSnapshot = Record<string, FeatureFlagState>

export type FeaturesMap = Record<string, boolean>

// Public read of effective feature flags. Available to any signed-in
// user so the frontend can hide sections that depend on a disabled
// pipeline stage (e.g. People when faces are off).
export const features = {
  list: async (): Promise<FeaturesMap> => {
    const response = await api.get('/features')
    return response.data
  },
}

export default api