import { useEffect, useMemo, useRef, useState } from 'react'
import {
  ChevronDown,
  ChevronRight,
  Folder as FolderIcon,
  Upload as UploadIcon,
  X,
  FileImage,
  CheckCircle2,
  AlertCircle,
} from 'lucide-react'
import clsx from 'clsx'
import { useQueryClient } from '@tanstack/react-query'
import { uploads, type FolderTreeNode } from '../../services/api'
import { useFolderTreeQuery } from '../../hooks/useFolderTreeQuery'
import { useHeapsQuery, HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery'
import { FOLDER_TREE_QUERY_KEY } from '../../hooks/useFolderTreeQuery'
import { LIBRARY_STATS_QUERY_KEY } from '../../hooks/useLibraryStatsQuery'
import { toast } from '../ToastContainer'
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog'

interface UploadModalProps {
  isOpen: boolean
  onClose: () => void
  /** Optional pre-selected destination. Accepts a Folder id or SourceRoot
   *  id — the backend resolves source roots to their root Folder row. */
  initialFolderId?: string | null
}

interface QueuedFile {
  /** Stable key; the browser may give us multiple files with the same
   *  name from different subfolders, so we key on index + path. */
  key: string
  file: File
  relativePath: string
  status: 'pending' | 'uploading' | 'done' | 'error'
  progress: number
  error?: string
}

const SUPPORTED_EXTENSIONS = new Set([
  '.jpg', '.jpeg', '.png', '.tiff', '.tif', '.webp', '.bmp',
  '.cr2', '.cr3', '.nef', '.arw', '.raf', '.dng', '.orf', '.rw2', '.pef', '.srw',
  '.heic', '.heif',
  '.mp4', '.mov', '.avi', '.mkv', '.mts', '.m2ts', '.3gp', '.wmv', '.flv',
])

function extOf(name: string): string {
  const i = name.lastIndexOf('.')
  return i === -1 ? '' : name.slice(i).toLowerCase()
}

function isSupported(name: string): boolean {
  return SUPPORTED_EXTENSIONS.has(extOf(name))
}

const MAX_PARALLEL = 4

/** Walk the tree to collect the ids on the path from root to `targetId`,
 *  excluding the target itself — used to expand ancestor rows so a
 *  pre-selected destination is visible. */
function collectAncestors(tree: FolderTreeNode[], targetId: string): string[] {
  const path: string[] = []
  const walk = (nodes: FolderTreeNode[], chain: string[]): boolean => {
    for (const n of nodes) {
      if (n.id === targetId) {
        path.push(...chain)
        return true
      }
      if (n.children && walk(n.children, [...chain, n.id])) return true
    }
    return false
  }
  walk(tree, [])
  return path
}

/**
 * Upload from desktop. Supports:
 *   - Dropping files or folders onto the drop zone
 *   - Picking files with "Select files"
 *   - Picking a whole folder with "Select folder" (webkitdirectory);
 *     every file's webkitRelativePath is sent to the backend so sub-
 *     folder structure is preserved under the chosen destination.
 *
 * Destination is a Folder row (or a source root, which the backend
 * resolves to its root folder). An optional heap can also be chosen —
 * uploaded photos are added to that heap in the same request.
 */
export function UploadModal({ isOpen, onClose, initialFolderId }: UploadModalProps) {
  const queryClient = useQueryClient()
  const { data: folderTree } = useFolderTreeQuery()
  const { data: allHeaps = [] } = useHeapsQuery()

  const [queue, setQueue] = useState<QueuedFile[]>([])
  const [destFolderId, setDestFolderId] = useState<string | null>(null)
  const [destHeapId, setDestHeapId] = useState<string | null>(null)
  const [isUploading, setIsUploading] = useState(false)
  const [dragOver, setDragOver] = useState(false)
  const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())

  const fileInputRef = useRef<HTMLInputElement>(null)
  const dirInputRef = useRef<HTMLInputElement>(null)
  const abortRef = useRef<AbortController | null>(null)

  // Default destination: caller-provided initialFolderId wins; otherwise
  // first source root in the tree. Re-runs when the modal is re-opened
  // with a different initial target so the right row is highlighted.
  useEffect(() => {
    if (!isOpen) return
    if (initialFolderId) {
      setDestFolderId(initialFolderId)
      // Expand every ancestor of the pre-selected folder so the row is
      // actually visible in the tree.
      if (folderTree) {
        const ancestors = collectAncestors(folderTree, initialFolderId)
        setExpandedFolders((prev) => new Set([...prev, ...ancestors]))
      }
      return
    }
    if (!destFolderId && folderTree && folderTree.length > 0) {
      setDestFolderId(folderTree[0].id)
      setExpandedFolders(new Set([folderTree[0].id]))
    }
  }, [isOpen, initialFolderId, folderTree, destFolderId])

  // Esc / overlay-click dismissal lives on the Dialog primitive below.
  // We only need to guard against closing while an upload is in flight.

  // Reset transient state on open so a previous session's queue doesn't
  // bleed into a fresh one.
  useEffect(() => {
    if (isOpen) {
      setQueue([])
      setIsUploading(false)
    }
  }, [isOpen])

  const addFiles = (incoming: File[], relPathFn?: (f: File) => string) => {
    const next: QueuedFile[] = []
    let skipped = 0
    for (const file of incoming) {
      const relPath = (relPathFn?.(file) ?? '').replace(/\\/g, '/').replace(/^\/+/, '')
      const filename = relPath || file.name
      if (!isSupported(filename)) {
        skipped++
        continue
      }
      next.push({
        key: `${relPath || file.name}::${file.size}::${file.lastModified}::${next.length}`,
        file,
        relativePath: relPath,
        status: 'pending',
        progress: 0,
      })
    }
    if (skipped > 0) {
      toast.info?.(`Skipped ${skipped} unsupported file${skipped === 1 ? '' : 's'}`)
    }
    setQueue((prev) => [...prev, ...next])
  }

  const handleFilePick = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files ?? [])
    addFiles(files, (f) => f.name) // no relative path for single files
    e.target.value = '' // allow re-picking the same file
  }

  const handleDirPick = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files ?? [])
    addFiles(files, (f) => (f as File & { webkitRelativePath?: string }).webkitRelativePath || f.name)
    e.target.value = ''
  }

  // Drag-and-drop handler. We walk the DataTransferItemList recursively
  // with webkitGetAsEntry so dropped folders contribute every nested
  // file, with relative paths reconstructed from the entry chain.
  const handleDrop = async (e: React.DragEvent) => {
    e.preventDefault()
    setDragOver(false)
    const items = Array.from(e.dataTransfer.items)
    const collected: { file: File; relativePath: string }[] = []

    const walkEntry = (entry: any, pathPrefix: string): Promise<void> => {
      return new Promise((resolve) => {
        if (!entry) return resolve()
        if (entry.isFile) {
          entry.file((f: File) => {
            collected.push({
              file: f,
              relativePath: pathPrefix ? `${pathPrefix}/${entry.name}` : '',
            })
            resolve()
          }, () => resolve())
        } else if (entry.isDirectory) {
          const reader = entry.createReader()
          const readBatch = () => {
            reader.readEntries(async (entries: any[]) => {
              if (!entries.length) return resolve()
              const childPrefix = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name
              await Promise.all(entries.map((c) => walkEntry(c, childPrefix)))
              // readEntries only returns a batch at a time; loop until empty.
              readBatch()
            }, () => resolve())
          }
          readBatch()
        } else {
          resolve()
        }
      })
    }

    await Promise.all(
      items.map((it) => {
        const entry = (it as DataTransferItem & { webkitGetAsEntry?: () => any }).webkitGetAsEntry?.()
        return walkEntry(entry, '')
      })
    )

    if (collected.length === 0) {
      // Fallback for browsers without webkitGetAsEntry — use plain files.
      const files = Array.from(e.dataTransfer.files)
      addFiles(files, (f) => f.name)
      return
    }

    const files = collected.map((c) => c.file)
    const pathMap = new Map<File, string>(collected.map((c) => [c.file, c.relativePath]))
    addFiles(files, (f) => pathMap.get(f) || f.name)
  }

  const removeFromQueue = (key: string) => {
    setQueue((prev) => prev.filter((q) => q.key !== key))
  }

  const startUpload = async () => {
    if (!destFolderId || queue.length === 0) return
    setIsUploading(true)
    const ctrl = new AbortController()
    abortRef.current = ctrl

    // Simple worker-pool: up to MAX_PARALLEL concurrent uploads.
    const pending = queue.filter((q) => q.status === 'pending' || q.status === 'error')
    let cursor = 0
    let successCount = 0
    let failCount = 0

    const uploadOne = async (item: QueuedFile) => {
      setQueue((prev) =>
        prev.map((q) => (q.key === item.key ? { ...q, status: 'uploading', progress: 0, error: undefined } : q))
      )
      try {
        await uploads.uploadFile(item.file, destFolderId, {
          relativePath: item.relativePath || undefined,
          heapId: destHeapId,
          signal: ctrl.signal,
          onProgress: (loaded, total) => {
            const pct = total > 0 ? loaded / total : 0
            setQueue((prev) =>
              prev.map((q) => (q.key === item.key ? { ...q, progress: pct } : q))
            )
          },
        })
        successCount++
        setQueue((prev) =>
          prev.map((q) => (q.key === item.key ? { ...q, status: 'done', progress: 1 } : q))
        )
      } catch (err: any) {
        failCount++
        const msg = err?.response?.data?.detail || err?.message || 'Upload failed'
        setQueue((prev) =>
          prev.map((q) => (q.key === item.key ? { ...q, status: 'error', error: msg } : q))
        )
      }
    }

    const workers: Promise<void>[] = []
    for (let i = 0; i < Math.min(MAX_PARALLEL, pending.length); i++) {
      workers.push(
        (async () => {
          while (cursor < pending.length && !ctrl.signal.aborted) {
            const idx = cursor++
            await uploadOne(pending[idx])
          }
        })()
      )
    }
    await Promise.all(workers)

    setIsUploading(false)
    abortRef.current = null

    // Refresh everything affected by new photos.
    queryClient.invalidateQueries({ queryKey: FOLDER_TREE_QUERY_KEY })
    queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY })
    queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
    queryClient.invalidateQueries({ queryKey: ['photos'] })

    if (successCount > 0) {
      toast.success(
        `Uploaded ${successCount} file${successCount === 1 ? '' : 's'}`,
        failCount > 0 ? `${failCount} failed — see list` : undefined
      )
    }
    if (successCount === 0 && failCount > 0) {
      toast.error('Upload failed', `${failCount} file${failCount === 1 ? '' : 's'} errored`)
    }
  }

  const cancelUpload = () => {
    abortRef.current?.abort()
  }

  const totalBytes = useMemo(() => queue.reduce((s, q) => s + q.file.size, 0), [queue])
  const uploadedBytes = useMemo(
    () => queue.reduce((s, q) => s + q.file.size * (q.status === 'done' ? 1 : q.progress), 0),
    [queue]
  )
  const overallPct = totalBytes > 0 ? Math.round((uploadedBytes / totalBytes) * 100) : 0

  return (
    <Dialog
      open={isOpen}
      onOpenChange={(o) => {
        if (!o && !isUploading) onClose()
      }}
    >
      <DialogContent className="flex max-h-[85vh] w-[760px] max-w-[760px] flex-col p-0">
        <DialogHeader className="border-b border-border px-5 py-3">
          <DialogTitle className="flex items-center gap-2">
            <UploadIcon className="h-4 w-4 text-text-muted" />
            Upload photos
          </DialogTitle>
        </DialogHeader>

          {/* Body */}
          <div className="flex min-h-0 flex-1 gap-4 overflow-hidden p-5">
            {/* Left: destination picker */}
            <div className="flex w-64 flex-col gap-4 overflow-hidden">
              <div className="flex flex-col gap-2 overflow-hidden">
                <label className="text-xs font-medium uppercase tracking-wide text-text-muted">
                  Destination folder
                </label>
                <div className="flex-1 overflow-auto rounded border border-border bg-bg p-1 text-sm">
                  {folderTree && folderTree.length > 0 ? (
                    folderTree.map((n) => (
                      <FolderTreeRow
                        key={n.id}
                        node={n}
                        depth={0}
                        selectedId={destFolderId}
                        onSelect={setDestFolderId}
                        expanded={expandedFolders}
                        onToggle={(id) =>
                          setExpandedFolders((prev) => {
                            const next = new Set(prev)
                            if (next.has(id)) next.delete(id)
                            else next.add(id)
                            return next
                          })
                        }
                      />
                    ))
                  ) : (
                    <div className="p-3 text-xs text-text-muted">No folders yet.</div>
                  )}
                </div>
              </div>

              <div className="flex flex-col gap-2">
                <label className="text-xs font-medium uppercase tracking-wide text-text-muted">
                  Also add to heap (optional)
                </label>
                <select
                  value={destHeapId ?? ''}
                  onChange={(e) => setDestHeapId(e.target.value || null)}
                  className="rounded border border-border bg-bg px-2 py-1.5 text-sm text-text"
                >
                  <option value="">— none —</option>
                  {allHeaps.map((h) => (
                    <option key={h.id} value={h.id}>
                      {h.name}
                    </option>
                  ))}
                </select>
              </div>
            </div>

            {/* Right: drop zone + queue */}
            <div className="flex min-w-0 flex-1 flex-col gap-3 overflow-hidden">
              <div
                onDragOver={(e) => {
                  e.preventDefault()
                  setDragOver(true)
                }}
                onDragLeave={() => setDragOver(false)}
                onDrop={handleDrop}
                className={clsx(
                  'flex flex-col items-center justify-center rounded border-2 border-dashed px-4 py-6 text-center transition-colors',
                  dragOver
                    ? 'border-primary bg-primary/10'
                    : 'border-border bg-bg'
                )}
              >
                <UploadIcon className="mb-2 h-6 w-6 text-text-muted" />
                <div className="text-sm text-text">
                  Drop files or folders here
                </div>
                <div className="mt-1 text-xs text-text-muted">
                  Folder structure is preserved under the destination.
                </div>
                <div className="mt-3 flex gap-2">
                  <button
                    type="button"
                    onClick={() => fileInputRef.current?.click()}
                    className="rounded border border-border px-3 py-1 text-xs text-text hover:bg-surface-2"
                  >
                    Select files
                  </button>
                  <button
                    type="button"
                    onClick={() => dirInputRef.current?.click()}
                    className="rounded border border-border px-3 py-1 text-xs text-text hover:bg-surface-2"
                  >
                    Select folder
                  </button>
                </div>
                <input
                  ref={fileInputRef}
                  type="file"
                  multiple
                  accept="image/*,video/*,.heic,.heif,.cr2,.cr3,.nef,.arw,.raf,.dng,.orf,.rw2,.pef,.srw"
                  className="hidden"
                  onChange={handleFilePick}
                />
                <input
                  ref={dirInputRef}
                  type="file"
                  multiple
                  // @ts-expect-error — non-standard but supported in Chromium/WebKit
                  webkitdirectory=""
                  directory=""
                  className="hidden"
                  onChange={handleDirPick}
                />
              </div>

              {/* Queue */}
              <div className="min-h-0 flex-1 overflow-auto rounded border border-border bg-bg">
                {queue.length === 0 ? (
                  <div className="flex h-full items-center justify-center text-xs text-text-muted">
                    No files added yet.
                  </div>
                ) : (
                  <ul className="divide-y divide-border">
                    {queue.map((item) => (
                      <li key={item.key} className="flex items-center gap-2 px-3 py-2 text-sm">
                        <FileImage className="h-4 w-4 shrink-0 text-text-muted" />
                        <div className="min-w-0 flex-1">
                          <div className="truncate text-text">
                            {item.relativePath || item.file.name}
                          </div>
                          {item.status === 'uploading' && (
                            <div className="mt-1 h-1 w-full overflow-hidden rounded bg-surface-2">
                              <div
                                className="h-full bg-primary transition-all"
                                style={{ width: `${Math.round(item.progress * 100)}%` }}
                              />
                            </div>
                          )}
                          {item.status === 'error' && item.error && (
                            <div className="mt-0.5 truncate text-xs text-reject">{item.error}</div>
                          )}
                        </div>
                        <div className="shrink-0">
                          {item.status === 'done' && <CheckCircle2 className="h-4 w-4 text-green-500" />}
                          {item.status === 'error' && <AlertCircle className="h-4 w-4 text-reject" />}
                          {item.status !== 'done' && !isUploading && (
                            <button
                              onClick={() => removeFromQueue(item.key)}
                              className="rounded p-0.5 text-text-muted hover:bg-surface-2 hover:text-text"
                              aria-label="Remove"
                            >
                              <X className="h-3.5 w-3.5" />
                            </button>
                          )}
                        </div>
                      </li>
                    ))}
                  </ul>
                )}
              </div>

              {queue.length > 0 && (
                <div className="text-xs text-text-muted">
                  {queue.length} file{queue.length === 1 ? '' : 's'} •{' '}
                  {(totalBytes / 1024 / 1024).toFixed(1)} MB
                  {isUploading && ` • ${overallPct}% uploaded`}
                </div>
              )}
            </div>
          </div>

          {/* Footer */}
          <div className="flex items-center justify-end gap-2 border-t border-border px-5 py-3">
            <button
              onClick={isUploading ? cancelUpload : onClose}
              className="rounded border border-border px-3 py-1.5 text-sm text-text hover:bg-surface-2"
            >
              {isUploading ? 'Cancel' : 'Close'}
            </button>
            <button
              onClick={startUpload}
              disabled={isUploading || queue.length === 0 || !destFolderId}
              className="flex items-center gap-1.5 rounded bg-primary px-3 py-1.5 text-sm font-medium text-white hover:bg-primary/80 disabled:cursor-not-allowed disabled:opacity-50"
            >
              <UploadIcon className="h-3.5 w-3.5" />
              {isUploading ? 'Uploading…' : `Upload ${queue.length || ''}`.trim()}
            </button>
          </div>
      </DialogContent>
    </Dialog>
  )
}

interface FolderTreeRowProps {
  node: FolderTreeNode
  depth: number
  selectedId: string | null
  onSelect: (id: string) => void
  expanded: Set<string>
  onToggle: (id: string) => void
}

function FolderTreeRow({
  node,
  depth,
  selectedId,
  onSelect,
  expanded,
  onToggle,
}: FolderTreeRowProps) {
  const isExpanded = expanded.has(node.id)
  const hasChildren = node.children && node.children.length > 0
  const isSelected = selectedId === node.id

  return (
    <>
      <div
        className={clsx(
          'flex cursor-pointer items-center gap-1 rounded px-1 py-1 text-sm',
          isSelected ? 'bg-primary/20 text-text' : 'text-text-muted hover:bg-surface-2 hover:text-text'
        )}
        style={{ paddingLeft: `${depth * 12 + 4}px` }}
        onClick={() => onSelect(node.id)}
      >
        <button
          onClick={(e) => {
            e.stopPropagation()
            if (hasChildren) onToggle(node.id)
          }}
          className="flex h-4 w-4 items-center justify-center"
          aria-label={isExpanded ? 'Collapse' : 'Expand'}
        >
          {hasChildren ? (
            isExpanded ? (
              <ChevronDown className="h-3 w-3" />
            ) : (
              <ChevronRight className="h-3 w-3" />
            )
          ) : null}
        </button>
        <FolderIcon className="h-3.5 w-3.5 shrink-0" />
        <span className="truncate">{node.name}</span>
      </div>
      {isExpanded &&
        node.children?.map((c) => (
          <FolderTreeRow
            key={c.id}
            node={c}
            depth={depth + 1}
            selectedId={selectedId}
            onSelect={onSelect}
            expanded={expanded}
            onToggle={onToggle}
          />
        ))}
    </>
  )
}
