import { useEffect, useMemo, useState } from 'react'
import {
  X,
  Star,
  MapPin,
  Camera,
  Aperture,
  ChevronDown,
  ChevronRight,
  ShoppingBasket,
  Trash2,
} from 'lucide-react'
import clsx from 'clsx'
import {
  useQuery,
  useMutation,
  useQueryClient,
  keepPreviousData,
} from '@tanstack/react-query'
import {
  photos as photosApi,
  heaps as heapsApi,
  tags as tagsApi,
} from '../../services/api'
import { useActiveHeapMembers } from '../../hooks/useActiveHeapMembersQuery'
import { HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery'
import { LIBRARY_STATS_QUERY_KEY } from '../../hooks/useLibraryStatsQuery'
import { useTagsQuery, TAGS_QUERY_KEY } from '../../hooks/useTagsQuery'
import { formatApiError } from '../../lib/apiError'
import { toast } from '../ToastContainer'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
  COLOR_LABEL_OPTIONS,
  type ColorLabel,
} from '../../constants/colorLabels'
import { toDatetimeLocalValue } from '../../lib/guessDateFromPath'
import { TagsEditor } from './TagsEditor'
import { TakenAtEditor } from './TakenAtEditor'

export interface PhotoTagSummary {
  id: string
  name: string
  color: string | null
}

export interface PhotoDetails {
  id: string
  filename: string
  filepath: string
  width: number | null
  height: number | null
  file_size: number | null
  taken_at: string | null
  taken_at_source: string | null
  rating: number
  is_discarded: boolean
  user_title: string | null
  user_notes: string | null
  color_label: string | null
  exif_json: string | null
  latitude?: number | null
  longitude?: number | null
  tags?: PhotoTagSummary[]
}

/** Format a signed decimal degree value with the hemisphere letter, e.g.
 *  ``48.12777° N``. Keeps the panel readable without dragging in a heavy
 *  formatting lib. */
function formatLatLon(value: number, axis: 'lat' | 'lon'): string {
  const abs = Math.abs(value).toFixed(5)
  const ref = axis === 'lat' ? (value >= 0 ? 'N' : 'S') : (value >= 0 ? 'E' : 'W')
  return `${abs}° ${ref}`
}

interface ExifData {
  Make?: string
  Model?: string
  LensModel?: string
  Lens?: string
  ISO?: number | string
  FNumber?: number | string
  ApertureValue?: number | string
  ExposureTime?: string
  ShutterSpeedValue?: string
  FocalLength?: string
  FocalLengthIn35mmFormat?: string
  GPSLatitude?: number | string
  GPSLongitude?: number | string
  [key: string]: unknown
}

function formatFileSize(bytes: number | null): string {
  if (bytes == null) return '—'
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
  return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
}

function formatExifValue(v: unknown): string {
  if (v == null || v === '') return '—'
  return String(v)
}

function pickFirst(exif: ExifData, ...keys: string[]): string {
  for (const k of keys) {
    const v = exif[k]
    if (v != null && v !== '') return String(v)
  }
  return '—'
}

function parseExif(json: string | null): ExifData {
  if (!json) return {}
  try {
    const parsed = JSON.parse(json)
    return typeof parsed === 'object' && parsed !== null ? (parsed as ExifData) : {}
  } catch {
    return {}
  }
}

interface PhotoInfoPanelProps {
  /** The photo to show metadata for. Drives an on-demand detail fetch. */
  photoId: string
  /** When true, the editable text fields (filename, title, notes) render
   *  with a darker theme to read against a black preview backdrop. */
  darkTheme?: boolean
}

/**
 * Reusable single-photo metadata + edit panel. Used by both the grid
 * RightSidebar (when one photo is selected) and the PreviewView's optional
 * info overlay. Self-contained — owns its own queries and mutations.
 */
export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelProps) {
  const queryClient = useQueryClient()

  const [expandedSections, setExpandedSections] = useState<Set<string>>(
    new Set(['basic', 'camera', 'location', 'tags'])
  )
  const toggleSection = (section: string) => {
    const next = new Set(expandedSections)
    if (next.has(section)) next.delete(section)
    else next.add(section)
    setExpandedSections(next)
  }

  // Fetch the photo's full record (with EXIF) on demand. `keepPreviousData`
  // holds the last photo on screen while the next one loads, so arrow-nav
  // through the preview doesn't flash the "Loading…" placeholder between
  // every neighbour — the panel swaps in place once the new record arrives.
  const { data: photo, isPlaceholderData } = useQuery<PhotoDetails>({
    queryKey: ['photo', photoId],
    queryFn: () => photosApi.get(photoId),
    enabled: !!photoId,
    staleTime: 60_000,
    placeholderData: keepPreviousData,
  })

  // Mutation for any patchable field. Invalidates both the photo detail
  // cache and the timeline list so the grid reflects the change too.
  const updateMutation = useMutation({
    mutationFn: (data: {
      filename?: string
      rating?: number
      is_discarded?: boolean
      user_title?: string | null
      user_notes?: string | null
      color_label?: string | null
      taken_at?: string
    }) => photosApi.update(photoId, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['photo', photoId] })
      queryClient.invalidateQueries({ queryKey: ['photos'] })
      queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
    },
  })

  // Active heap membership for the Pick toggle button.
  const { activeHeap, memberIds: activeHeapMembers } = useActiveHeapMembers()
  const isInActiveHeap = activeHeapMembers.has(photoId)

  const heapMutation = useMutation({
    mutationFn: ({ remove }: { remove: boolean }) => {
      if (!activeHeap) return Promise.resolve(null)
      return remove
        ? heapsApi.removePhotos(activeHeap.id, [photoId])
        : heapsApi.addPhotos(activeHeap.id, [photoId])
    },
    onMutate: ({ remove }) => {
      if (!activeHeap) return { previous: undefined }
      const key = ['heap-photo-ids', activeHeap.id] as const
      const previous = queryClient.getQueryData<string[]>(key)
      const set = new Set(previous ?? [])
      if (remove) set.delete(photoId)
      else set.add(photoId)
      queryClient.setQueryData<string[]>(key, Array.from(set))
      return { previous }
    },
    onError: (_e, _vars, ctx) => {
      if (activeHeap && ctx?.previous) {
        queryClient.setQueryData(['heap-photo-ids', activeHeap.id], ctx.previous)
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY })
      if (activeHeap) {
        queryClient.invalidateQueries({
          queryKey: ['heap-photo-ids', activeHeap.id],
        })
      }
    },
  })

  // ── Tags state + mutations ──────────────────────────────────────────
  const { data: allTags = [] } = useTagsQuery()
  const [tagInput, setTagInput] = useState('')

  const invalidateTagsAndPhoto = () => {
    queryClient.invalidateQueries({ queryKey: TAGS_QUERY_KEY })
    queryClient.invalidateQueries({ queryKey: ['photo', photoId] })
    queryClient.invalidateQueries({ queryKey: ['photos'] })
    queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
  }

  const addTagMutation = useMutation({
    mutationFn: async (name: string) => {
      const created = await tagsApi.create(name)
      await tagsApi.addToPhoto(photoId, [created.id])
      return created
    },
    onSuccess: () => invalidateTagsAndPhoto(),
    onError: (e: any) =>
      toast.error('Add tag failed', formatApiError(e)),
  })

  const attachExistingTagMutation = useMutation({
    mutationFn: (tagId: string) => tagsApi.addToPhoto(photoId, [tagId]),
    onSuccess: () => invalidateTagsAndPhoto(),
    onError: (e: any) =>
      toast.error('Add tag failed', formatApiError(e)),
  })

  const removeTagMutation = useMutation({
    mutationFn: (tagId: string) => tagsApi.removeFromPhoto(photoId, tagId),
    onSuccess: () => invalidateTagsAndPhoto(),
    onError: (e: any) =>
      toast.error('Remove tag failed', formatApiError(e)),
  })

  // Local drafts for the text fields. Mirror the server value but stay
  // independent while typing so we don't fight focus or clobber edits.
  const [filenameDraft, setFilenameDraft] = useState('')
  const [titleDraft, setTitleDraft] = useState('')
  const [notesDraft, setNotesDraft] = useState('')
  const [takenAtDraft, setTakenAtDraft] = useState('')

  useEffect(() => {
    setFilenameDraft(photo?.filename ?? '')
    setTitleDraft(photo?.user_title ?? '')
    setNotesDraft(photo?.user_notes ?? '')
    setTakenAtDraft(
      photo?.taken_at ? toDatetimeLocalValue(new Date(photo.taken_at)) : ''
    )
  }, [
    photo?.id,
    photo?.filename,
    photo?.user_title,
    photo?.user_notes,
    photo?.taken_at,
  ])

  const commitFilename = () => {
    const next = filenameDraft.trim()
    const current = photo?.filename ?? ''
    if (!next || next === current) {
      setFilenameDraft(current)
      return
    }
    if (next.includes('/') || next.includes('\\') || next === '.' || next === '..') {
      toast.error('Invalid filename', 'No path separators allowed')
      setFilenameDraft(current)
      return
    }
    updateMutation.mutate(
      { filename: next },
      {
        onError: (e: any) => {
          toast.error(
            'Rename failed',
            formatApiError(e)
          )
          setFilenameDraft(current)
        },
      }
    )
  }

  const commitTitle = () => {
    const next = titleDraft.trim()
    const current = photo?.user_title ?? ''
    if (next === current) return
    updateMutation.mutate({ user_title: next || null })
  }

  const commitNotes = () => {
    const next = notesDraft
    const current = photo?.user_notes ?? ''
    if (next === current) return
    updateMutation.mutate({ user_notes: next || null })
  }

  /** Commit a datetime-local draft back to the server. The backend also
   *  rewrites EXIF on disk, so a failure here rolls the draft back to the
   *  server value — we never want the UI to silently disagree with the
   *  file. An empty string is a no-op because the input's `required` is
   *  off and we don't yet have a "clear date" affordance. */
  const commitTakenAt = (rawValue?: string) => {
    const source = rawValue ?? takenAtDraft
    if (!source) return
    const parsed = new Date(source)
    if (Number.isNaN(parsed.getTime())) {
      toast.error('Invalid date', 'Could not parse the value')
      setTakenAtDraft(
        photo?.taken_at ? toDatetimeLocalValue(new Date(photo.taken_at)) : ''
      )
      return
    }
    const iso = parsed.toISOString()
    if (photo?.taken_at && new Date(photo.taken_at).toISOString() === iso) {
      return
    }
    updateMutation.mutate(
      { taken_at: iso },
      {
        onError: (e: any) => {
          toast.error(
            'Date update failed',
            formatApiError(e)
          )
          setTakenAtDraft(
            photo?.taken_at
              ? toDatetimeLocalValue(new Date(photo.taken_at))
              : ''
          )
        },
      }
    )
  }

  const exif = useMemo(() => parseExif(photo?.exif_json ?? null), [photo?.exif_json])

  if (!photo) {
    return <div className="p-4 text-xs text-text-muted">Loading…</div>
  }

  const rating = photo.rating ?? 0
  const isDiscarded = photo.is_discarded ?? false
  const colorLabel = (photo.color_label ?? null) as ColorLabel | null

  // Single themable input class so the same component reads against either
  // the surface (grid sidebar) or a darker preview overlay.
  const inputClass = clsx(
    'w-full rounded border px-2 py-1 text-sm focus:outline-none',
    darkTheme
      ? 'border-white/15 bg-black/40 text-white placeholder-white/40 focus:border-primary'
      : 'border-border bg-bg text-text placeholder-text-faint focus:border-primary'
  )
  const monoInputClass = clsx(
    'w-full rounded border px-2 py-1 font-mono text-xs focus:outline-none',
    darkTheme
      ? 'border-white/15 bg-black/40 text-white placeholder-white/40 focus:border-primary'
      : 'border-border bg-bg text-text placeholder-text-faint focus:border-primary'
  )

  return (
    <div
      className={clsx(
        'flex h-full flex-col transition-opacity duration-150',
        isPlaceholderData && 'opacity-70'
      )}
    >
      {/* Edit fields */}
      <div className="space-y-2.5 border-b border-border p-3">
        <div>
          <label className="mb-1 block text-xs text-text-muted">Filename</label>
          <Input
            type="text"
            value={filenameDraft}
            onChange={(e) => setFilenameDraft(e.target.value)}
            onBlur={commitFilename}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                e.currentTarget.blur()
              } else if (e.key === 'Escape') {
                setFilenameDraft(photo.filename ?? '')
                e.currentTarget.blur()
              }
            }}
            className={monoInputClass}
          />
        </div>

        <div>
          <label className="mb-1 block text-xs text-text-muted">Title</label>
          <Input
            type="text"
            value={titleDraft}
            onChange={(e) => setTitleDraft(e.target.value)}
            onBlur={commitTitle}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                e.currentTarget.blur()
              } else if (e.key === 'Escape') {
                setTitleDraft(photo.user_title ?? '')
                e.currentTarget.blur()
              }
            }}
            placeholder="No title"
            className={inputClass}
          />
        </div>

        <div>
          <label className="mb-1 block text-xs text-text-muted">Notes</label>
          <Textarea
            value={notesDraft}
            onChange={(e) => setNotesDraft(e.target.value)}
            onBlur={commitNotes}
            placeholder="Add notes…"
            rows={3}
            className={clsx(inputClass, 'resize-none')}
          />
        </div>

        {/* Rating */}
        <div>
          <label className="mb-1 block text-xs text-text-muted">Rating</label>
          <div className="flex gap-1">
            {[1, 2, 3, 4, 5].map((value) => (
              <button
                key={value}
                onClick={() =>
                  updateMutation.mutate({ rating: rating === value ? 0 : value })
                }
                className="p-0.5"
                title={`Set rating to ${value}`}
              >
                <Star
                  className={clsx(
                    'h-5 w-5 transition-colors',
                    value <= rating
                      ? 'fill-star text-star'
                      : 'text-text-muted hover:text-star'
                  )}
                />
              </button>
            ))}
          </div>
        </div>

        {/* Color label */}
        <div>
          <label className="mb-1 block text-xs text-text-muted">Color label</label>
          <div className="flex items-center gap-1.5">
            {COLOR_LABEL_OPTIONS.map(({ value, className }) => {
              const active = colorLabel === value
              return (
                <button
                  key={value}
                  onClick={() =>
                    updateMutation.mutate({ color_label: active ? null : value })
                  }
                  className={clsx(
                    'h-5 w-5 rounded-full ring-offset-2 ring-offset-surface transition-all',
                    className,
                    active ? 'ring-2 ring-primary' : 'opacity-60 hover:opacity-100'
                  )}
                  title={value}
                />
              )
            })}
            {colorLabel && (
              <button
                onClick={() => updateMutation.mutate({ color_label: null })}
                className="ml-1 rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
                title="Clear color label"
              >
                <X className="h-3 w-3" />
              </button>
            )}
          </div>
        </div>

        {/* Flag — Select + Discard */}
        <div>
          <label className="mb-1 block text-xs text-text-muted">Flag</label>
          <div className="flex gap-2">
            <button
              onClick={() => {
                if (!activeHeap) return
                heapMutation.mutate({ remove: isInActiveHeap })
              }}
              disabled={!activeHeap || heapMutation.isPending}
              className={clsx(
                'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50',
                isInActiveHeap
                  ? 'bg-pick/20 text-pick'
                  : 'bg-surface-2 text-text-muted hover:bg-surface-offset'
              )}
              title={
                activeHeap
                  ? isInActiveHeap
                    ? `Remove from "${activeHeap.name}"`
                    : `Add to "${activeHeap.name}"`
                  : 'Set an active heap first'
              }
            >
              <ShoppingBasket className="h-3 w-3" />
              {isInActiveHeap ? 'Selected' : 'Select'}
            </button>
            <button
              onClick={() => updateMutation.mutate({ is_discarded: !isDiscarded })}
              className={clsx(
                'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors',
                isDiscarded
                  ? 'bg-reject/20 text-reject'
                  : 'bg-surface-2 text-text-muted hover:bg-surface-offset'
              )}
            >
              <Trash2 className="h-3 w-3" />
              Discard
            </button>
          </div>
        </div>
      </div>

      {/* Read-only metadata sections */}
      <div className="flex-1 overflow-y-auto">
        <Section
          title="Tags"
          expanded={expandedSections.has('tags')}
          onToggle={() => toggleSection('tags')}
        >
          <TagsEditor
            photoTags={photo.tags ?? []}
            allTags={allTags}
            tagInput={tagInput}
            onTagInputChange={setTagInput}
            onAttachExisting={(id) => attachExistingTagMutation.mutate(id)}
            onCreateAndAttach={(name) => {
              addTagMutation.mutate(name)
              setTagInput('')
            }}
            onRemove={(id) => removeTagMutation.mutate(id)}
          />
        </Section>

        <Section
          title="Basic Info"
          expanded={expandedSections.has('basic')}
          onToggle={() => toggleSection('basic')}
        >
          <div className="grid grid-cols-2 gap-2 text-xs">
            <Field label="Size" value={formatFileSize(photo.file_size)} />
            <Field
              label="Dimensions"
              value={
                photo.width && photo.height
                  ? `${photo.width} × ${photo.height}`
                  : '—'
              }
            />
          </div>
          <TakenAtEditor
            photo={photo}
            draft={takenAtDraft}
            onDraftChange={setTakenAtDraft}
            onCommit={commitTakenAt}
            darkTheme={darkTheme}
          />
          {/* Filepath spans the full sidebar width — most paths are long
           *  enough that the two-column grid above wraps them painfully.
           *  Mono so each character lines up under the next, break-all
           *  so we never overflow horizontally on a long basename. */}
          <div className="mt-2 text-xs">
            <span className="text-text-muted">Path:</span>
            <p className="mt-0.5 break-all font-mono text-[11px] text-text" title={photo.filepath}>
              {photo.filepath || '—'}
            </p>
          </div>
        </Section>

        <Section
          title="Camera"
          expanded={expandedSections.has('camera')}
          onToggle={() => toggleSection('camera')}
        >
          <div className="space-y-1 text-xs">
            <div className="flex items-center gap-2">
              <Camera className="h-3 w-3 text-text-muted" />
              <span className="text-text">
                {pickFirst(exif, 'Make', 'Model') === '—'
                  ? '—'
                  : `${formatExifValue(exif.Make)} ${formatExifValue(exif.Model)}`.trim()}
              </span>
            </div>
            <div className="flex items-center gap-2">
              <Aperture className="h-3 w-3 text-text-muted" />
              <span className="text-text">
                {pickFirst(exif, 'LensModel', 'Lens')}
              </span>
            </div>
            <div className="mt-2 grid grid-cols-2 gap-2">
              <Field label="ISO" value={formatExifValue(exif.ISO)} />
              <Field
                label="Aperture"
                value={
                  exif.FNumber
                    ? `f/${exif.FNumber}`
                    : pickFirst(exif, 'ApertureValue')
                }
              />
              <Field
                label="Shutter"
                value={pickFirst(exif, 'ExposureTime', 'ShutterSpeedValue')}
              />
              <Field
                label="Focal"
                value={pickFirst(
                  exif,
                  'FocalLength',
                  'FocalLengthIn35mmFormat'
                )}
              />
            </div>
          </div>
        </Section>

        <Section
          title="Location"
          expanded={expandedSections.has('location')}
          onToggle={() => toggleSection('location')}
        >
          {photo.latitude != null && photo.longitude != null ? (
            <a
              href={`https://www.openstreetmap.org/?mlat=${photo.latitude}&mlon=${photo.longitude}#map=15/${photo.latitude}/${photo.longitude}`}
              target="_blank"
              rel="noreferrer"
              className="flex items-center gap-2 text-xs hover:underline"
              title="Open in OpenStreetMap"
            >
              <MapPin className="h-3 w-3 text-text-muted" />
              <span className="font-mono text-text">
                {formatLatLon(photo.latitude, 'lat')},{' '}
                {formatLatLon(photo.longitude, 'lon')}
              </span>
            </a>
          ) : (
            <div className="text-xs text-text-muted">No GPS data</div>
          )}
        </Section>

      </div>
    </div>
  )
}

function Section({
  title,
  expanded,
  onToggle,
  children,
}: {
  title: string
  expanded: boolean
  onToggle: () => void
  children: React.ReactNode
}) {
  return (
    <Collapsible
      open={expanded}
      onOpenChange={onToggle}
      className="border-b border-border"
    >
      <CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-text-muted hover:bg-surface-2 hover:text-text">
        <span>{title}</span>
        {expanded ? (
          <ChevronDown className="h-3 w-3" />
        ) : (
          <ChevronRight className="h-3 w-3" />
        )}
      </CollapsibleTrigger>
      <CollapsibleContent className="px-3 pb-2.5">
        {children}
      </CollapsibleContent>
    </Collapsible>
  )
}

function Field({ label, value }: { label: string; value: string }) {
  return (
    <div>
      <span className="text-text-muted">{label}:</span>
      <p className="break-words text-text">{value}</p>
    </div>
  )
}

