import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { api, postDownload, sha256Hex, uploadFile } from '../api'
import Gallery from '../components/Gallery'
import Lightbox from '../components/Lightbox'
import SelectionBar, { fmtBytes } from '../components/SelectionBar'
import Stars from '../components/Stars'
import Thumbs from '../components/Thumbs'
import useLightbox from '../useLightbox'
import useSelection from '../useSelection'

function fmtEta(seconds) {
  if (!isFinite(seconds) || seconds < 0) return ''
  if (seconds < 60) return `${Math.ceil(seconds)}s`
  if (seconds < 3600) return `${Math.ceil(seconds / 60)} min`
  return `${Math.floor(seconds / 3600)}h ${Math.ceil((seconds % 3600) / 60)} min`
}

const UPLOAD_CONCURRENCY = 3

const FEEDBACK_FILTERS = [
  { key: 'all', label: 'All' },
  { key: 'accept', label: '👍' },
  { key: 'reject', label: '👎' },
  { key: 'undecided', label: 'Undecided' },
]

// Expiry convention, in one place for the create form and the row editor:
// end of the chosen day in the photographer's local timezone — date-only
// strings would parse as UTC midnight and expire a day early.
const endOfDayIso = (day) => (day ? new Date(`${day}T23:59:59`).toISOString() : null)

// ISO timestamp -> local yyyy-mm-dd for date inputs.
const localDate = (iso) => {
  const d = new Date(iso)
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}

// Staged expiry editor: commits on blur/Enter, never per keystroke — typing
// a year fires change events with bogus intermediate dates (year 0002) that
// must not hit the live link. The ✕ clears explicitly; Safari's date input
// has no native clear control.
function ExpiryDate({ value, onCommit }) {
  const current = value ? localDate(value) : ''
  const [draft, setDraft] = useState(current)
  useEffect(() => setDraft(current), [current])

  const commit = () => {
    if (draft === current) return
    // A half-typed year (e.g. 0002) can survive until blur; don't persist it.
    if (draft && draft.slice(0, 4) < '2000') {
      setDraft(current)
      return
    }
    onCommit(endOfDayIso(draft))
  }

  return (
    <label className="row field-label" title="Expiry date — empty means the link never expires">
      <span className="muted">expires</span>
      <input
        type="date"
        value={draft}
        onChange={(e) => setDraft(e.target.value)}
        onBlur={commit}
        onKeyDown={(e) => {
          if (e.key === 'Enter') e.target.blur()
        }}
      />
      {draft ? (
        <button
          type="button"
          className="btn btn-ghost"
          title="Remove expiry — link never expires"
          onClick={() => {
            setDraft('')
            onCommit(null)
          }}
        >
          ✕
        </button>
      ) : (
        <span className="muted">never</span>
      )}
    </label>
  )
}

function UploadZone({ albumId, onUploaded }) {
  const [queue, setQueue] = useState([])
  const [dragging, setDragging] = useState(false)
  const [speed, setSpeed] = useState(0)
  const inputRef = useRef(null)
  const running = useRef(0)
  const pending = useRef([])
  const lastRefresh = useRef(0)
  const loadedRef = useRef(0)

  const totalBytes = queue.reduce((sum, item) => sum + item.file.size, 0)
  const loadedBytes = queue.reduce(
    (sum, item) =>
      sum + (item.status === 'done' ? item.file.size : (item.progress || 0) * item.file.size),
    0,
  )
  loadedRef.current = loadedBytes
  const active = queue.some((item) =>
    ['uploading', 'queued', 'checking'].includes(item.status),
  )

  // Sample throughput once a second (EMA-smoothed) while uploads run.
  useEffect(() => {
    if (!active) {
      setSpeed(0)
      return
    }
    let last = { loaded: loadedRef.current, time: Date.now() }
    const timer = setInterval(() => {
      const now = Date.now()
      const instant = (loadedRef.current - last.loaded) / ((now - last.time) / 1000)
      last = { loaded: loadedRef.current, time: now }
      setSpeed((prev) => (prev > 0 ? prev * 0.7 + instant * 0.3 : instant))
    }, 1000)
    return () => clearInterval(timer)
  }, [active])

  // Refresh the album at most every 5s during a bulk upload (the processing
  // poll keeps it fresh anyway), plus once when the queue drains.
  const refresh = useCallback(() => {
    const drained = running.current === 0 && pending.current.length === 0
    if (drained || Date.now() - lastRefresh.current > 5000) {
      lastRefresh.current = Date.now()
      onUploaded()
    }
  }, [onUploaded])

  const pump = useCallback(() => {
    while (running.current < UPLOAD_CONCURRENCY && pending.current.length > 0) {
      const item = pending.current.shift()
      running.current += 1
      const update = (patch) =>
        setQueue((q) => q.map((x) => (x.key === item.key ? { ...x, ...patch } : x)))
      const transfer = async () => {
        // Hash locally first: content the album already has is skipped
        // without transferring a single byte.
        update({ status: 'checking' })
        try {
          const hash = await sha256Hex(item.file)
          await api(`/api/albums/${albumId}/photos/by-hash/${hash}`)
          update({ status: 'skipped', progress: 1 })
          return
        } catch {
          // 404 (not there yet) or hashing unavailable — upload normally.
        }
        update({ status: 'uploading' })
        const url = `/api/albums/${albumId}/photos?filename=${encodeURIComponent(item.file.name)}`
        await uploadFile(url, item.file, (p) => update({ progress: p }))
        update({ status: 'done', progress: 1 })
      }
      transfer()
        .catch((e) => update({ status: 'error', error: e.message }))
        .finally(() => {
          running.current -= 1
          refresh()
          pump()
        })
    }
  }, [albumId, refresh])

  const addFiles = (files) => {
    const items = [...files].map((file, i) => ({
      key: `${Date.now()}-${i}-${file.name}`,
      file,
      status: 'queued',
      progress: 0,
    }))
    if (items.length === 0) return
    setQueue((q) => [...q.filter((x) => x.status !== 'done'), ...items])
    pending.current.push(...items)
    pump()
  }

  return (
    <div
      className={`upload-zone${dragging ? ' dragging' : ''}`}
      onDragOver={(e) => {
        e.preventDefault()
        setDragging(true)
      }}
      onDragLeave={() => setDragging(false)}
      onDrop={(e) => {
        e.preventDefault()
        setDragging(false)
        addFiles(e.dataTransfer.files)
      }}
      onClick={() => inputRef.current?.click()}
    >
      <input
        ref={inputRef}
        type="file"
        multiple
        hidden
        onChange={(e) => {
          addFiles(e.target.files)
          e.target.value = ''
        }}
      />
      <p>Drop RAWs or JPGs here, or click to select</p>
      {queue.length > 0 && (
        <div className="upload-summary" onClick={(e) => e.stopPropagation()}>
          <progress className="upload-total" value={loadedBytes} max={totalBytes || 1} />
          <span className="muted">
            {queue.filter((i) => i.status === 'done' || i.status === 'skipped').length} /{' '}
            {queue.length} files ·{' '}
            {fmtBytes(loadedBytes)} of {fmtBytes(totalBytes)}
            {active && speed > 0 && (
              <>
                {' · '}
                {fmtBytes(speed)}/s · ~{fmtEta((totalBytes - loadedBytes) / speed)} left
              </>
            )}
          </span>
        </div>
      )}
      {queue.length > 0 && (
        <ul className="upload-list" onClick={(e) => e.stopPropagation()}>
          {queue.map((item) => (
            <li key={item.key} className={`upload-item ${item.status}`}>
              <span className="upload-name">{item.file.name}</span>
              {item.status === 'error' ? (
                <span className="error">{item.error}</span>
              ) : item.status === 'skipped' ? (
                <span className="muted">already uploaded</span>
              ) : item.status === 'checking' ? (
                <span className="muted">checking…</span>
              ) : (
                <progress value={item.progress} max="1" />
              )}
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

function SharesPanel({ albumId }) {
  const [shares, setShares] = useState([])
  const [form, setForm] = useState({ label: '', password: '', allow_download: true, expires_at: '' })
  const [error, setError] = useState(null)
  const [copied, setCopied] = useState(null)

  const load = useCallback(
    () => api(`/api/albums/${albumId}/shares`).then(setShares).catch((e) => setError(e.message)),
    [albumId],
  )
  useEffect(() => {
    load()
  }, [load])

  const create = async (e) => {
    e.preventDefault()
    try {
      await api(`/api/albums/${albumId}/shares`, {
        method: 'POST',
        body: {
          label: form.label,
          password: form.password || null,
          allow_download: form.allow_download,
          expires_at: endOfDayIso(form.expires_at),
        },
      })
      setForm({ label: '', password: '', allow_download: true, expires_at: '' })
      setError(null)
      load()
    } catch (e) {
      setError(e.message)
    }
  }

  const copy = async (share) => {
    await navigator.clipboard.writeText(share.url)
    setCopied(share.id)
    setTimeout(() => setCopied(null), 1500)
  }

  const update = async (shareId, patch) => {
    try {
      const updated = await api(`/api/shares/${shareId}`, { method: 'PATCH', body: patch })
      setShares((list) => list.map((s) => (s.id === shareId ? updated : s)))
      setError(null)
    } catch (e) {
      setError(e.message)
      // Re-sync the controlled inputs with what the server actually has.
      load()
    }
  }

  return (
    <section className="panel">
      <h2>Client links</h2>
      {shares.length === 0 && <p className="muted">No links yet.</p>}
      {shares.map((s) => (
        <div key={s.id} className="share-row">
          <div className="share-info">
            <strong>
              {s.label || 'unnamed link'}
              {s.locked && <span className="error"> — locked (too many wrong passwords)</span>}
            </strong>
            <span className="muted">
              {s.has_password ? '🔒 password' : 'no password'}
              {' · '}
              {s.rating_count} ratings, {s.tag_count} tags, 👍 {s.accept_count} 👎{' '}
              {s.reject_count}
            </span>
          </div>
          <div className="row">
            <label className="select-toggle" title="Allow this link to download originals/ZIPs">
              <input
                type="checkbox"
                checked={s.allow_download}
                onChange={(e) => update(s.id, { allow_download: e.target.checked })}
              />
              downloads
            </label>
            <ExpiryDate value={s.expires_at} onCommit={(iso) => update(s.id, { expires_at: iso })} />
            {s.locked && (
              <button
                className="btn"
                onClick={async () => {
                  await api(`/api/shares/${s.id}/reset-lock`, { method: 'POST' })
                  load()
                }}
              >
                Unlock
              </button>
            )}
            <button className="btn" onClick={() => copy(s)}>
              {copied === s.id ? 'Copied!' : 'Copy link'}
            </button>
            <button
              className="btn btn-danger"
              onClick={async () => {
                if (!confirm(`Delete link "${s.label || s.token}"? Client ratings and tags from this link are removed too.`)) return
                await api(`/api/shares/${s.id}`, { method: 'DELETE' })
                load()
              }}
            >
              Delete
            </button>
          </div>
        </div>
      ))}
      <form className="share-form" onSubmit={create}>
        <input
          placeholder="Label (e.g. client name)"
          value={form.label}
          onChange={(e) => setForm({ ...form, label: e.target.value })}
        />
        <input
          placeholder="Password (optional)"
          value={form.password}
          onChange={(e) => setForm({ ...form, password: e.target.value })}
        />
        <label className="row field-label">
          <span className="muted">Expires</span>
          <input
            type="date"
            value={form.expires_at}
            onChange={(e) => setForm({ ...form, expires_at: e.target.value })}
          />
          {form.expires_at ? (
            <button
              type="button"
              className="btn btn-ghost"
              title="Remove expiry — link never expires"
              onClick={() => setForm({ ...form, expires_at: '' })}
            >
              ✕
            </button>
          ) : (
            <span className="muted">never</span>
          )}
        </label>
        <label className="row">
          <input
            type="checkbox"
            checked={form.allow_download}
            onChange={(e) => setForm({ ...form, allow_download: e.target.checked })}
          />
          allow downloads
        </label>
        <button className="btn btn-primary" type="submit">
          Create link
        </button>
      </form>
      {error && <p className="error">{error}</p>}
    </section>
  )
}

export default function AlbumPage() {
  const { id } = useParams()
  const navigate = useNavigate()
  const [detail, setDetail] = useState(null)
  const [error, setError] = useState(null)

  const load = useCallback(
    () => api(`/api/albums/${id}`).then(setDetail).catch((e) => setError(e.message)),
    [id],
  )
  useEffect(() => {
    load()
  }, [load])

  // Poll while any photo is still being processed by the workers.
  const hasPending = detail?.photos.some((p) => p.status === 'uploaded' || p.status === 'processing')
  useEffect(() => {
    if (!hasPending) return
    const t = setInterval(load, 4000)
    return () => clearInterval(t)
  }, [hasPending, load])

  const ready = (detail?.photos ?? []).filter((p) => p.status === 'ready')
  const feedback = detail?.feedback ?? {}

  const avgRating = (photoId) => {
    const ratings = feedback[photoId]?.ratings || []
    if (ratings.length === 0) return null
    return ratings.reduce((sum, r) => sum + r.rating, 0) / ratings.length
  }

  // Two combinable filter dimensions over client feedback: verdict chips
  // plus a minimum-average-stars threshold (0 = off).
  const [filter, setFilter] = useState('all')
  const [minStars, setMinStars] = useState(0)
  const matchesVerdict = (p, key) => {
    if (key === 'all') return true
    const verdicts = feedback[p.id]?.verdicts || []
    if (key === 'accept') return verdicts.some((v) => v.verdict === 'accept')
    if (key === 'reject') return verdicts.some((v) => v.verdict === 'reject')
    return verdicts.length === 0
  }
  const shown = ready.filter(
    (p) => matchesVerdict(p, filter) && (minStars === 0 || (avgRating(p.id) ?? 0) >= minStars),
  )
  const filterCounts = useMemo(() => {
    const counts = { all: ready.length, accept: 0, reject: 0, undecided: 0 }
    for (const p of ready) {
      const verdicts = feedback[p.id]?.verdicts || []
      if (verdicts.some((v) => v.verdict === 'accept')) counts.accept += 1
      if (verdicts.some((v) => v.verdict === 'reject')) counts.reject += 1
      if (verdicts.length === 0) counts.undecided += 1
    }
    return counts
  }, [detail])

  // Selection spans all ready photos; the bar and bulk actions cover only
  // the current view, like on the share page.
  const { selected, toggle, selectAll, clear } = useSelection(ready)
  const shownSelected = shown.filter((p) => selected.has(p.id))
  const sumBytes = (list) => list.reduce((sum, p) => sum + p.size_bytes, 0)
  const lightbox = useLightbox(shown)

  if (error) return <p className="error">{error}</p>
  if (!detail) return <p className="muted">Loading…</p>

  const { album, photos } = detail
  const notReady = photos.filter((p) => p.status !== 'ready')

  const rename = async () => {
    const name = prompt('Album name', album.name)
    if (name && name.trim()) {
      await api(`/api/albums/${id}`, { method: 'PATCH', body: { name: name.trim() } })
      load()
    }
  }

  const removeAlbum = async () => {
    if (!confirm(`Delete album "${album.name}" and all ${photos.length} photos? This cannot be undone.`)) return
    await api(`/api/albums/${id}`, { method: 'DELETE' })
    navigate('/')
  }

  const removePhoto = async (photoId) => {
    if (!confirm('Delete this photo?')) return
    lightbox.close()
    await api(`/api/photos/${photoId}`, { method: 'DELETE' })
    load()
  }

  const removeSelected = async () => {
    const ids = shownSelected.map((p) => p.id)
    if (!confirm(`Delete ${ids.length} selected photo${ids.length === 1 ? '' : 's'}? This cannot be undone.`))
      return
    try {
      await api('/api/photos/delete', { method: 'POST', body: { ids } })
      clear()
    } catch (e) {
      alert(`Delete failed: ${e.message}`)
    }
    load()
  }

  return (
    <>
      <div className="page-head">
        <h1>
          <Link to="/" className="muted">
            Albums /
          </Link>{' '}
          {album.name}
        </h1>
        <div className="row">
          <button className="btn" onClick={rename}>
            Rename
          </button>
          <button className="btn btn-danger" onClick={removeAlbum}>
            Delete album
          </button>
        </div>
      </div>

      <UploadZone albumId={id} onUploaded={load} />

      {notReady.length > 0 && (
        <section className="panel">
          <h2>Processing</h2>
          <ul className="pending-list">
            {notReady.map((p) => (
              <li key={p.id}>
                <span className="upload-name">{p.filename}</span>
                {p.status === 'error' ? (
                  <span className="row">
                    <span className="error">{p.error || 'failed'}</span>
                    <button
                      className="btn"
                      onClick={() => api(`/api/photos/${p.id}/reprocess`, { method: 'POST' }).then(load)}
                    >
                      Retry
                    </button>
                    <button className="btn btn-danger" onClick={() => removePhoto(p.id)}>
                      Delete
                    </button>
                  </span>
                ) : (
                  <span className="muted">{p.status}…</span>
                )}
              </li>
            ))}
          </ul>
        </section>
      )}

      {ready.length > 0 && (
        <div className="filter-bar filter-bar-admin">
          {FEEDBACK_FILTERS.map((f) => (
            <button
              key={f.key}
              className={`filter-chip${filter === f.key ? ' active' : ''}`}
              onClick={() => setFilter(f.key)}
            >
              {f.label} {filterCounts[f.key]}
            </button>
          ))}
          <span
            className={`filter-stars${minStars > 0 ? ' active' : ''}`}
            title="Minimum average rating — click a star to filter, click it again to clear"
          >
            <Stars value={minStars} onChange={(n) => setMinStars(n)} small />
            {minStars > 0 && <span className="muted">≥ {minStars}</span>}
          </span>
        </div>
      )}

      <Gallery
        photos={shown}
        onOpen={lightbox.openAt}
        selected={selected}
        onToggleSelect={(photoId, shift) => toggle(photoId, shift, shown)}
        overlay={(p) => {
          const avg = avgRating(p.id)
          const tagCount = feedback[p.id]?.tags.length || 0
          const verdicts = feedback[p.id]?.verdicts || []
          const accepts = verdicts.filter((v) => v.verdict === 'accept').length
          const rejects = verdicts.length - accepts
          if (avg === null && tagCount === 0 && accepts === 0 && rejects === 0) return null
          return (
            <div className="g-overlay">
              {accepts > 0 && <span>👍 {accepts}</span>}
              {rejects > 0 && <span>👎 {rejects}</span>}
              {avg !== null && <span>★ {avg.toFixed(1)}</span>}
              {tagCount > 0 && <span># {tagCount}</span>}
            </div>
          )
        }}
      />
      {ready.length === 0 && notReady.length === 0 && (
        <p className="muted">No photos yet — drop some above.</p>
      )}
      {ready.length > 0 && shown.length === 0 && (
        <p className="muted">No photos match this filter.</p>
      )}

      <SelectionBar
        count={shownSelected.length}
        total={shown.length}
        selectedBytes={sumBytes(shownSelected)}
        totalBytes={sumBytes(shown)}
        onSelectAll={() => selectAll(shown)}
        onClear={clear}
        onDownload={() =>
          postDownload(`/api/albums/${id}/zip`, shownSelected.map((p) => p.id).join(','))
        }
        onDownloadAll={() =>
          postDownload(`/api/albums/${id}/zip`, filter === 'all' ? '' : shown.map((p) => p.id).join(','))
        }
        onDelete={removeSelected}
      />

      {lightbox.index >= 0 && (
        <Lightbox
          photos={shown}
          index={lightbox.index}
          onClose={lightbox.close}
          onNav={lightbox.openAt}
          actions={[
            { keys: ['s'], help: ['S', 'select for download'], run: (p) => toggle(p.id) },
          ]}
          footer={(p) => {
            const fb = feedback[p.id] || { ratings: [], verdicts: [], tags: [] }
            return (
              <div className="admin-footer">
                <div className="feedback">
                  {fb.ratings.length === 0 && fb.verdicts.length === 0 && fb.tags.length === 0 && (
                    <span className="muted">No client feedback yet</span>
                  )}
                  {fb.verdicts.map((v, i) => (
                    <span key={`v${i}`} className="feedback-item">
                      {v.share_label || 'client'}: <Thumbs value={v.verdict} small />
                    </span>
                  ))}
                  {fb.ratings.map((r, i) => (
                    <span key={`r${i}`} className="feedback-item">
                      {r.share_label || 'client'}: <Stars value={r.rating} small />
                    </span>
                  ))}
                  {fb.tags.map((t, i) => (
                    <span key={`t${i}`} className="chip">
                      {t.tag} <em className="muted">({t.share_label || 'client'})</em>
                    </span>
                  ))}
                </div>
                <div className="row">
                  <label className="select-toggle">
                    <input
                      type="checkbox"
                      checked={selected.has(p.id)}
                      onChange={() => toggle(p.id)}
                    />
                    select
                  </label>
                  <a className="btn" href={`/api/photos/${p.id}/original`}>
                    Download original
                  </a>
                  <button className="btn btn-danger" onClick={() => removePhoto(p.id)}>
                    Delete
                  </button>
                </div>
              </div>
            )
          }}
        />
      )}

      <SharesPanel albumId={id} />
    </>
  )
}
