import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { api, postDownload } from '../api'
import Gallery from '../components/Gallery'
import Lightbox from '../components/Lightbox'
import Modal from '../components/Modal'
import SelectionBar from '../components/SelectionBar'
import SharesPanel from '../components/SharesPanel'
import Stars from '../components/Stars'
import Thumbs, { ACCEPT_GLYPH, REJECT_GLYPH } from '../components/Thumbs'
import UploadZone from '../components/UploadZone'
import XmpModal from '../components/XmpModal'
import useCulling from '../useCulling'
import useLightbox from '../useLightbox'
import useSelection from '../useSelection'

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

// Feedback regrouped per client link for the lightbox footer — the
// meaningful reading unit is "what did this person say".
const clientFeedback = (fb) => {
  const map = new Map()
  const entry = (id, label) => {
    if (!map.has(id)) map.set(id, { label: label || 'client', tags: [] })
    return map.get(id)
  }
  for (const r of fb.ratings) entry(r.share_id, r.share_label).rating = r.rating
  for (const v of fb.verdicts) entry(v.share_id, v.share_label).verdict = v.verdict
  for (const t of fb.tags) entry(t.share_id, t.share_label).tags.push(t.tag)
  return [...map.entries()]
}

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])

  // While workers are busy, poll the lightweight pending endpoint and only
  // refetch the full album when something actually changed. The signature
  // lives in a ref so votes/patches don't restart (and starve) the interval.
  const notReady = (detail?.photos ?? []).filter((p) => p.status !== 'ready')
  const hasPending = notReady.some((p) => p.status === 'uploaded' || p.status === 'processing')
  const signature = (list) => list.map((p) => `${p.id}:${p.status}`).sort().join()
  const pendingSig = useRef('')
  pendingSig.current = signature(notReady)
  useEffect(() => {
    if (!hasPending) return
    const t = setInterval(async () => {
      try {
        const status = await api(`/api/albums/${id}/pending`)
        if (signature(status.pending) !== pendingSig.current) load()
      } catch {
        // transient; next tick retries
      }
    }, 4000)
    return () => clearInterval(t)
  }, [hasPending, id, load])

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

  // Whose feedback the filters and overlays look at: null = everyone
  // (clients + own, server-aggregated), 'own', or a share id.
  const [client, setClient] = useState(null)

  // One pass per (album data, scope): everything downstream — filter,
  // counts, overlay — reads this map instead of re-deriving per render.
  const scoped = useMemo(() => {
    const map = new Map()
    for (const p of ready) {
      const fb = feedback[p.id]
      let top = null
      let accepts = 0
      let rejects = 0
      let tags = 0
      if (client === 'own') {
        top = p.owner_rating ?? null
        if (p.owner_verdict === 'accept') accepts = 1
        if (p.owner_verdict === 'reject') rejects = 1
      } else if (client) {
        for (const r of fb?.ratings ?? [])
          if (r.share_id === client) top = Math.max(top ?? 0, r.rating)
        for (const v of fb?.verdicts ?? [])
          if (v.share_id === client) v.verdict === 'accept' ? (accepts += 1) : (rejects += 1)
        for (const t of fb?.tags ?? []) if (t.share_id === client) tags += 1
      } else {
        const agg = aggregates[p.id]
        top = agg?.top_rating ?? null
        accepts = agg?.accepts ?? 0
        rejects = agg?.rejects ?? 0
        tags = fb?.tags.length ?? 0
      }
      map.set(p.id, { top, accepts, rejects, tags })
    }
    return map
  }, [detail, client])

  const [filter, setFilter] = useState('all')
  const [minStars, setMinStars] = useState(0)
  const [linksOpen, setLinksOpen] = useState(false)
  const [xmpOpen, setXmpOpen] = useState(false)

  const matchesVerdict = (s, key) => {
    if (key === 'all') return true
    if (key === 'accept') return s.accepts > 0
    if (key === 'reject') return s.rejects > 0
    return s.accepts + s.rejects === 0
  }
  const shown = useMemo(
    () =>
      ready.filter((p) => {
        const s = scoped.get(p.id)
        return matchesVerdict(s, filter) && (minStars === 0 || (s.top ?? 0) >= minStars)
      }),
    [ready, scoped, filter, minStars],
  )
  const filterCounts = useMemo(() => {
    const counts = { all: ready.length, accept: 0, reject: 0, undecided: 0 }
    for (const p of ready) {
      const s = scoped.get(p.id)
      if (s.accepts > 0) counts.accept += 1
      if (s.rejects > 0) counts.reject += 1
      if (s.accepts + s.rejects === 0) counts.undecided += 1
    }
    return counts
  }, [ready, scoped])

  const clientTotals = useMemo(() => {
    const totals = new Map()
    const entry = (id, label) => {
      if (!totals.has(id))
        totals.set(id, { label: label || 'client', accepts: 0, rejects: 0, ratings: 0, tags: 0 })
      return totals.get(id)
    }
    for (const fb of Object.values(feedback)) {
      for (const r of fb.ratings) entry(r.share_id, r.share_label).ratings += 1
      for (const v of fb.verdicts)
        entry(v.share_id, v.share_label)[v.verdict === 'accept' ? 'accepts' : 'rejects'] += 1
      for (const t of fb.tags) entry(t.share_id, t.share_label).tags += 1
    }
    return [...totals.entries()]
  }, [detail])

  const ownTotals = useMemo(() => {
    const totals = { accepts: 0, rejects: 0, ratings: 0, tags: 0 }
    for (const p of ready) {
      if (p.owner_verdict === 'accept') totals.accepts += 1
      if (p.owner_verdict === 'reject') totals.rejects += 1
      if (p.owner_rating) totals.ratings += 1
    }
    return totals
  }, [ready])

  // The active scope's pill stays visible even at zero counts, so clearing
  // your last vote can't strand the filter without a control.
  const strip = [
    ['own', { label: 'you', ...ownTotals }],
    ...clientTotals,
  ].filter(([key, t]) => key === client || t.accepts + t.rejects + t.ratings + t.tags > 0)

  // Reset only when the scoped share is gone entirely (deleted link).
  useEffect(() => {
    if (client && client !== 'own' && !clientTotals.some(([id]) => id === client)) setClient(null)
  }, [client, clientTotals])

  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)

  const patchPhoto = (photoId, patch) =>
    setDetail((d) => ({
      ...d,
      photos: d.photos.map((p) => (p.id === photoId ? { ...p, ...patch } : p)),
    }))

  // Per-photo sequence: only the latest request's aggregate may land, so
  // out-of-order responses can't overwrite fresher state.
  const aggSeq = useRef(new Map())
  const saveOwner = async (photo, patch, endpoint, body) => {
    patchPhoto(photo.id, patch)
    const seq = (aggSeq.current.get(photo.id) ?? 0) + 1
    aggSeq.current.set(photo.id, seq)
    try {
      const res = await api(`/api/photos/${photo.id}/${endpoint}`, { method: 'PUT', body })
      if (aggSeq.current.get(photo.id) !== seq) return
      setDetail((d) => ({
        ...d,
        aggregates: { ...d.aggregates, [photo.id]: res.aggregate },
      }))
    } catch {
      if (aggSeq.current.get(photo.id) === seq) load()
    }
  }
  const setOwnerRating = (photo, rating) =>
    saveOwner(photo, { owner_rating: rating || null }, 'rating', { rating })
  const setOwnerVerdict = (photo, verdict) =>
    saveOwner(photo, { owner_verdict: verdict }, 'verdict', { verdict })

  const culling = useCulling({
    visible: lightbox.view,
    lightbox,
    setVerdict: setOwnerVerdict,
    setRating: setOwnerRating,
    toggle,
  })

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

  const { album, photos } = detail

  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">
          <UploadZone
            albumId={id}
            onUploaded={load}
            processing={notReady}
            onRetry={(p) => api(`/api/photos/${p.id}/reprocess`, { method: 'POST' }).then(load)}
            onDelete={removePhoto}
          />
          <button className="btn" onClick={() => setLinksOpen(true)}>
            Client links
          </button>
          <button className="btn" onClick={() => setXmpOpen(true)}>
            XMP
          </button>
          <button className="btn" onClick={rename}>
            Rename
          </button>
          <button className="btn btn-danger" onClick={removeAlbum}>
            Delete album
          </button>
        </div>
      </div>

      {strip.length > 0 && (
        <div className="feedback-strip">
          {strip.map(([key, t]) => (
            <button
              key={key}
              className={`client-fb client-fb-btn${client === key ? ' active' : ''}`}
              title={
                client === key
                  ? 'Show everyone’s feedback again'
                  : `Filter by ${t.label === 'you' ? 'your' : `${t.label}’s`} feedback`
              }
              onClick={() => setClient((c) => (c === key ? null : key))}
            >
              <span className="muted">{t.label}</span>
              {t.accepts > 0 && (
                <span className="chip chip-accept">
                  {ACCEPT_GLYPH} {t.accepts}
                </span>
              )}
              {t.rejects > 0 && (
                <span className="chip chip-reject">
                  {REJECT_GLYPH} {t.rejects}
                </span>
              )}
              {t.ratings > 0 && <span className="chip">★ {t.ratings}</span>}
              {t.tags > 0 && <span className="chip"># {t.tags}</span>}
            </button>
          ))}
        </div>
      )}

      {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 top rating in the selected scope — 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)}
        keyboard={lightbox.index < 0 && !linksOpen && !xmpOpen}
        externalIndex={lightbox.index}
        actions={culling.keyActions}
        overlay={(p) => {
          const s = scoped.get(p.id)
          if (!s || (s.top === null && s.tags === 0 && s.accepts === 0 && s.rejects === 0))
            return null
          return (
            <div className="g-overlay">
              {s.accepts > 0 && <span>👍 {s.accepts}</span>}
              {s.rejects > 0 && <span>👎 {s.rejects}</span>}
              {s.top !== null && <span>{'★'.repeat(s.top)}</span>}
              {s.tags > 0 && <span># {s.tags}</span>}
            </div>
          )
        }}
      />
      {ready.length === 0 && notReady.length === 0 && (
        <p className="muted">No photos yet — use Upload or drop files anywhere on the page.</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={lightbox.view}
          index={lightbox.index}
          onClose={lightbox.close}
          onNav={lightbox.openAt}
          {...culling.lightboxProps}
          footer={(p) => {
            const groups = clientFeedback(feedback[p.id] || { ratings: [], verdicts: [], tags: [] })
            return (
              <div className="admin-footer">
                <div className="admin-actions">
                  <span className="own-feedback">
                    <Thumbs value={p.owner_verdict} onChange={(v) => setOwnerVerdict(p, v)} />
                    <Stars value={p.owner_rating || 0} onChange={(r) => setOwnerRating(p, r)} />
                  </span>
                  <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 className="feedback">
                  {groups.length === 0 && <span className="muted">No client feedback yet</span>}
                  {groups.map(([shareId, c]) => (
                    <span
                      key={shareId}
                      className={`client-fb${c.verdict ? ` client-fb-${c.verdict}` : ''}`}
                    >
                      <span className="muted">{c.label}</span>
                      {c.verdict && (
                        <span className="client-fb-verdict">
                          {c.verdict === 'accept' ? ACCEPT_GLYPH : REJECT_GLYPH}
                        </span>
                      )}
                      {c.rating && <Stars value={c.rating} small />}
                      {c.tags.map((tag) => (
                        <span key={tag} className="chip">
                          {tag}
                        </span>
                      ))}
                    </span>
                  ))}
                </div>
              </div>
            )
          }}
        />
      )}

      {linksOpen && (
        <Modal title="Client links" onClose={() => setLinksOpen(false)}>
          <SharesPanel albumId={id} />
        </Modal>
      )}
      {xmpOpen && <XmpModal albumId={id} onClose={() => setXmpOpen(false)} />}
      {culling.flash && (
        <div key={culling.flash.key} className="action-flash">
          {culling.flash.text}
        </div>
      )}
    </>
  )
}
