// Catalog.jsx — the §7 left pane.
//
// One scrollable flat list of every super-draft and active entry,
// state-styled rather than grouped. Filter chips are AND-combined per
// §7.1; search is fuzzy over title + slug + id. The "Pending ideas"
// disclosure per §7.3 lives at the bottom, above the "+ Propose New RFC"
// button.

import { useEffect, useMemo, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
import { listRFCs, listProposals, getCollection, bulkEntryMeta } from '../api'
import { entryPath, proposalPath, useProjectId, useCollectionId } from '../lib/entryPaths'
import JoinRequestModal from './JoinRequestModal.jsx'
import FacetGroups from './FacetGroups.jsx'
import BulkActionBar from './BulkActionBar.jsx'
import { showToast } from './ToastHost.jsx'

const STATE_CHIPS = [
  { id: 'super-draft', label: 'Super-draft' },
  { id: 'active',      label: 'Active' },
]

const SORT_OPTIONS = [
  { id: 'recent',  label: 'Recently active' },
  { id: 'title',   label: 'Title' },
  { id: 'id',      label: 'ID' },
  { id: 'state',   label: 'State' },
]

export default function Catalog({ viewer, onProposeRFC, version }) {
  const [rfcs, setRfcs] = useState([])
  const [proposals, setProposals] = useState([])
  // §22 S4: the viewer's contribute capability in this collection, driving the
  // propose-first empty state (C3.5) and the propose control. `null` until the
  // collection's caps load; we fall back to "any authenticated viewer" so the
  // common case doesn't flicker, then refine.
  const [canContribute, setCanContribute] = useState(null)
  // §22.8: when the viewer can't contribute here but holds no role reaching the
  // collection, offer "Request to join" in the footer.
  const [canRequestJoin, setCanRequestJoin] = useState(false)
  const [joinOpen, setJoinOpen] = useState(false)
  // §22.4a: the type-driven entry noun ("RFC" | "Spec" | "Feature") for this
  // collection, read from the API. Defaults to the generic "RFC" until loaded.
  const [entryNoun, setEntryNoun] = useState('RFC')
  const [search, setSearch] = useState('')
  const [sort, setSort] = useState('recent')
  const [activeChips, setActiveChips] = useState(new Set())
  const [pendingOpen, setPendingOpen] = useState(true)
  // §22.4a SLICE-3: faceted left pane. When the collection declares a `fields:`
  // schema, the catalog renders faceted groups (counts from the server) instead
  // of the legacy state chips, and re-fetches server-side as selections change.
  const [fields, setFields] = useState(null)        // collection field schema
  const [facets, setFacets] = useState({})          // { field: { value: count } }
  const [selections, setSelections] = useState({})  // { field: Set<string> }
  const [malformedOnly, setMalformedOnly] = useState(false)
  // §22.4a SLICE-5: multi-select for the bulk action bar (PUC-2). A Set of
  // selected slugs; the sticky bar appears when ≥1 is selected (faceted +
  // contributor only). Cleared on collection switch and after a bulk apply.
  const [selected, setSelected] = useState(() => new Set())
  const [loading, setLoading] = useState(false)
  const { slug, prNumber } = useParams()
  const pid = useProjectId()
  // §22 S2: the catalog is scoped to the active collection (the `/c/:cid/`
  // route segment, else the project's default collection).
  const cid = useCollectionId()

  // Collection-level data (proposals, caps, field schema) loads once per
  // collection — independent of the facet selections, which only re-fetch the
  // entry list. Switching collections clears any active facet selections.
  useEffect(() => {
    listProposals(pid).then(d => setProposals(d.items)).catch(() => setProposals([]))
    setCanContribute(null)
    setCanRequestJoin(false)
    setSelections({})
    setMalformedOnly(false)
    setSelected(new Set())
    getCollection(pid, cid)
      .then(c => {
        setCanContribute(!!c?.viewer?.can_contribute)
        setCanRequestJoin(!!c?.viewer?.can_request_join)
        setEntryNoun(c?.entry_noun || 'RFC')
        setFields(c?.fields || null)
      })
      .catch(() => { setCanContribute(false); setFields(null) })
  }, [version, pid, cid])

  // §22.4a SLICE-3: the entry list re-fetches server-side whenever the facet
  // selections or the malformed toggle change (in legacy mode selections stay
  // empty, so this fires once per collection like before).
  useEffect(() => {
    setLoading(true)
    const selObj = Object.fromEntries(
      Object.entries(selections).map(([f, set]) => [f, [...set]])
    )
    listRFCs(pid, cid, { selections: selObj, malformed: malformedOnly })
      .then(d => {
        setRfcs(d.items); setFacets(d.facets || {})
        // §22.4a SLICE-5: drop any bulk selections that the new (filtered)
        // list no longer contains, so the bar can't act on hidden rows.
        const visible = new Set(d.items.map(it => it.slug))
        setSelected(prev => {
          const kept = [...prev].filter(s => visible.has(s))
          return kept.length === prev.size ? prev : new Set(kept)
        })
      })
      .catch(() => { setRfcs([]); setFacets({}) })
      .finally(() => setLoading(false))
  }, [version, pid, cid, selections, malformedOnly])

  // While caps load, fall back to "any authenticated viewer" so the propose
  // affordance on the common (default-collection) case doesn't flash off.
  const mayPropose = canContribute === null ? !!viewer : canContribute

  // §22.4a SLICE-3: faceted mode when the collection declares a non-empty
  // `fields:` schema; otherwise the legacy state-chip catalog (INV-5).
  const faceted = !!fields && Object.keys(fields).length > 0

  const filtered = useMemo(() => {
    const needle = search.trim().toLowerCase()
    let items = rfcs.filter(r => {
      // In faceted mode the server already filtered by state; the legacy chips
      // narrow client-side only when there's no schema.
      if (!faceted && activeChips.size > 0 && !activeChips.has(r.state)) return false
      if (!needle) return true
      const hay = [r.title, r.slug, r.id || ''].join(' ').toLowerCase()
      return hay.includes(needle)
    })
    items = [...items].sort((a, b) => {
      // Starred items pin to the top of the current sort per §7.2.
      if (a.starred_by_me !== b.starred_by_me) return a.starred_by_me ? -1 : 1
      if (sort === 'title') return a.title.localeCompare(b.title)
      if (sort === 'id') return (a.id || 'zzz').localeCompare(b.id || 'zzz')
      if (sort === 'state') return a.state.localeCompare(b.state)
      // recent
      return (b.last_active_at || '').localeCompare(a.last_active_at || '')
    })
    return items
  }, [rfcs, search, sort, activeChips, faceted])

  function toggleChip(id) {
    const next = new Set(activeChips)
    next.has(id) ? next.delete(id) : next.add(id)
    setActiveChips(next)
  }

  // §22.4a SLICE-3 facet selection handlers (faceted mode).
  function toggleFacet(field, value) {
    setSelections(prev => {
      const next = { ...prev }
      const set = new Set(next[field] || [])
      set.has(value) ? set.delete(value) : set.add(value)
      if (set.size === 0) delete next[field]
      else next[field] = set
      return next
    })
  }
  function clearFilters() { setSelections({}); setMalformedOnly(false) }
  const hasActiveFilters = Object.keys(selections).length > 0 || malformedOnly

  // §22.4a SLICE-5: row selection + bulk apply (PUC-2).
  function toggleSelect(rowSlug) {
    setSelected(prev => {
      const next = new Set(prev)
      next.has(rowSlug) ? next.delete(rowSlug) : next.add(rowSlug)
      return next
    })
  }

  async function applyBulk({ op, field, value }) {
    const slugs = [...selected]
    if (slugs.length === 0) return
    try {
      const res = await bulkEntryMeta(pid, cid, { slugs, op, field, value })
      const nApplied = res.applied?.length || 0
      const nRejected = res.rejected?.length || 0
      showToast({
        summary: nRejected
          ? `${nApplied} updated, ${nRejected} skipped`
          : `${nApplied} updated`,
        category: nRejected ? 'warning' : 'success',
      })
      setSelected(new Set())
      // Re-fetch the list so the new values + facet counts reflect the change.
      const selObj = Object.fromEntries(
        Object.entries(selections).map(([f, set]) => [f, [...set]]))
      const d = await listRFCs(pid, cid, { selections: selObj, malformed: malformedOnly })
      setRfcs(d.items); setFacets(d.facets || {})
    } catch (e) {
      showToast({ summary: e.message || 'Bulk update failed', category: 'error' })
    }
  }

  return (
    <aside className="catalog">
      <div className="catalog-search">
        <input
          placeholder="Search title, slug, ID…"
          value={search}
          onChange={e => setSearch(e.target.value)}
        />
      </div>
      <div className="catalog-controls">
        <label htmlFor="catalog-sort">Sort:</label>
        <select id="catalog-sort" value={sort} onChange={e => setSort(e.target.value)}>
          {SORT_OPTIONS.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
        </select>
      </div>
      {faceted ? (
        <FacetGroups
          facets={facets}
          fields={fields}
          selections={selections}
          onToggle={toggleFacet}
          malformedOnly={malformedOnly}
          onToggleMalformed={() => setMalformedOnly(m => !m)}
          onClear={clearFilters}
          hasActiveFilters={hasActiveFilters}
        />
      ) : (
        <div className="catalog-chips">
          {STATE_CHIPS.map(chip => (
            <button
              key={chip.id}
              className={`chip ${activeChips.has(chip.id) ? 'active' : ''}`}
              onClick={() => toggleChip(chip.id)}
            >
              {chip.label}
            </button>
          ))}
        </div>
      )}

      {faceted && canContribute && selected.size > 0 && (
        <BulkActionBar
          fields={fields}
          count={selected.size}
          onApply={applyBulk}
          onClear={() => setSelected(new Set())}
        />
      )}

      <div className="catalog-list">
        {filtered.length === 0 ? (
          <div style={{ padding: '24px 14px', color: '#999', fontSize: 13 }}>
            {loading
              ? 'Loading…'
              // §22.4a SLICE-3: faceted mode with active filters → an
              // explicitly-clearable "no entries match" state (§5.1).
              : faceted && hasActiveFilters ? (
                <>
                  No entries match.{' '}
                  <button className="facet-clear" onClick={clearFilters}>Clear filters</button>
                </>
              )
              : rfcs.length === 0
              // C3.5: a contributor sees a propose-first call to action; a
              // granted viewer without contribute rights sees a bare empty
              // state; an anonymous reader sees the read-only note (the footer
              // carries the sign-in prompt).
              ? (mayPropose
                  ? 'No entries yet. Propose the first entry below.'
                  : 'No entries in the catalog yet.')
              : 'No matches.'}
          </div>
        ) : (
          filtered.map(r => {
            const isActive = slug === r.slug
            const isSuper = r.state === 'super-draft'
            // §22.4a SLICE-5: selectable rows in faceted mode for contributors.
            const selectable = faceted && canContribute
            const row = (
              <Link
                key={r.slug}
                to={entryPath(pid, r.slug, cid)}
                className={`catalog-row ${isActive ? 'active' : ''} ${isSuper ? 'is-super' : ''}`}
              >
                <div className="row-top">
                  <span className={`row-id ${isSuper ? 'super' : ''}`}>
                    {isSuper ? 'super-draft' : (r.id || '—')}
                  </span>
                  {r.metadata_malformed && (
                    <span className="row-malformed" title="Metadata fails this collection's schema">⚠ malformed</span>
                  )}
                </div>
                <span className="row-title">{r.title}</span>
                {r.tags.length > 0 && (
                  <span className="row-tags">{r.tags.join(' · ')}</span>
                )}
              </Link>
            )
            if (!selectable) return row
            return (
              <div key={r.slug} className="catalog-row-wrap selectable">
                <input
                  type="checkbox"
                  className="row-select"
                  aria-label={`select ${r.title}`}
                  checked={selected.has(r.slug)}
                  onChange={() => toggleSelect(r.slug)}
                />
                {row}
              </div>
            )
          })
        )}
      </div>

      <div className="catalog-pending">
        <button className="pending-header" onClick={() => setPendingOpen(o => !o)}>
          <span>{pendingOpen ? '▾' : '▸'} Pending ideas</span>
          <span className="pending-count">{proposals.length}</span>
        </button>
        {pendingOpen && proposals.length > 0 && (
          <div className="pending-list">
            {proposals.map(p => (
              <Link
                key={p.pr_number}
                to={proposalPath(pid, p.pr_number, cid)}
                className={`pending-row ${String(prNumber) === String(p.pr_number) ? 'active' : ''}`}
              >
                <div>{p.title.replace(/^Propose:\s*/, '')}</div>
                <div className="pending-by">by @{p.opened_by || '—'} · PR #{p.pr_number}</div>
              </Link>
            ))}
          </div>
        )}
        {pendingOpen && proposals.length === 0 && (
          <div style={{ padding: '0 14px 12px', color: '#aaa', fontSize: 12 }}>
            No pending proposals.
          </div>
        )}
      </div>

      <div className="catalog-footer">
        {viewer ? (
          // §22 S4: the propose control is offered only when the viewer may
          // contribute to *this* collection (the scope-role gate); a granted
          // viewer with no contribute right in this collection sees nothing.
          mayPropose ? (
            <button className="btn-propose" onClick={onProposeRFC}>+ Propose New {entryNoun}</button>
          ) : (
            // §22.8: signed in but no contribute right here — offer to join.
            canRequestJoin && (
              <button className="btn-propose" onClick={() => setJoinOpen(true)}>Request to join</button>
            )
          )
        ) : (
          <a className="btn-propose" href="/auth/login" title="Private beta — only invited emails can propose">
            Sign in to propose <span className="beta-chip">Beta</span>
          </a>
        )}
      </div>
      {joinOpen && (
        <JoinRequestModal
          scopeType="collection"
          scopeId={cid}
          onClose={() => setJoinOpen(false)}
        />
      )}
    </aside>
  )
}
