// PapersVal — Analysis Detail / Review Workspace
// Composition: TopBar + Sidebar + ContractViewer + ReviewPanel/FindingDetail

const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ---------- Auto-growing textarea ----------
function AutoTextarea({ value, onChange, className, placeholder, autoFocus, rows = 3, ...props }) {
  const ref = useRef(null);
  const resize = () => {
    const el = ref.current;
    if (!el) return;
    el.style.height = "auto";
    el.style.height = el.scrollHeight + "px";
  };
  useEffect(() => { resize(); }, [value]);
  return (
    <textarea
      ref={ref}
      className={className}
      placeholder={placeholder}
      autoFocus={autoFocus}
      rows={rows}
      value={value}
      onChange={(e) => { onChange(e); resize(); }}
      style={{ overflow: "hidden", resize: "none" }}
      {...props}
    />
  );
}

// ============================================================================
// Top bar
// ============================================================================
function TopBar({ data, sidebarOpen, onToggleSidebar, activeTemplate, onOpenUpload, onOpenTemplates, onReanalyze, onCopyLink, onExport }) {
  return (
    <header className="topbar">
      <button className="icon-btn" onClick={onToggleSidebar} aria-label={sidebarOpen ? "Hide history" : "Show history"} title="History">
        <Icon.Menu />
      </button>
      <div className="topbar-brand">
        <div className="brand-mark">L</div>
        <div className="brand-name">Legal</div>
      </div>
      <button className="btn btn-primary btn-newcontract" onClick={onOpenUpload} type="button">
        <Icon.Plus /> Analyze contract
      </button>
      <div className="topbar-divider"></div>
      <div className="topbar-doc">
        <div className="topbar-doc-title">{data.contract.filename}</div>
        <div className="topbar-doc-meta">
          <b>M-2026-118</b> · Acme onboarding · uploaded today, 10:42
        </div>
      </div>
      <div className="topbar-actions">
        <div className="template-cluster">
          <button className="template-pill" onClick={onOpenTemplates} type="button" title="Switch template">
            <span className="dot"></span>
            <span className="label">template</span>
            <span className="name">{activeTemplate.name}</span>
            <span className="caret"><Icon.ChevronDown /></span>
          </button>
          <button className="icon-btn template-reanalyze" onClick={onReanalyze} aria-label="Re-analyze with current template" title="Re-analyze">
            <Icon.Refresh />
          </button>
        </div>
        <button className="btn" onClick={onExport} type="button" title="Export DOCX">
          <Icon.Download />
        </button>
        <button className="btn btn-ghost" onClick={onCopyLink} type="button" title="Copy link">
          <Icon.Link />
        </button>
        <button className="btn btn-ghost btn-icon" type="button" aria-label="More">
          <Icon.More />
        </button>
      </div>
    </header>
  );
}

// ============================================================================
// Left sidebar
// ============================================================================
function Sidebar({ data, onNewAnalysis, onOpenPlaybook, activeAnalysisId, onSelectAnalysis }) {
  const grouped = useMemo(() => {
    const todayIds = new Set(["a-103"]);
    const yesterdayIds = new Set(["a-102", "a-101", "a-100"]);
    return {
      Today:     data.history.filter(h => todayIds.has(h.id)),
      Yesterday: data.history.filter(h => yesterdayIds.has(h.id)),
      Earlier:   data.history.filter(h => !todayIds.has(h.id) && !yesterdayIds.has(h.id))
    };
  }, [data.history]);

  return (
    <aside className="sidebar">
      <div className="sidebar-section">
        <div className="sidebar-label">History</div>
        <div className="history-search">
          <span className="glass"><Icon.Search /></span>
          <input placeholder="Search analyses…" />
        </div>
      </div>
      <div className="history-section">
        {Object.entries(grouped).map(([label, items]) => items.length ? (
          <div key={label}>
            <div className="history-day-label">{label}</div>
            {items.map(h => (
              <HistoryItem key={h.id} item={h} active={activeAnalysisId === h.id}
                onClick={() => onSelectAnalysis(h.id)} />
            ))}
          </div>
        ) : null)}
      </div>
    </aside>
  );
}

function HistoryItem({ item, active, onClick }) {
  const totalIssues = (item.high || 0) + (item.medium || 0) + (item.missing || 0);
  // Compact ratio: deviations/total checks
  const ratio = item.status === "ready"
    ? `${totalIssues}/40`
    : item.status === "comparing"
      ? `${item.progress}/${item.total}`
      : null;

  const dotClass = item.status === "ready" && item.high > 0   ? "has-high" :
                   item.status === "ready" && item.missing > 0 ? "has-missing" :
                   item.status === "comparing" ? "comparing" :
                   item.status === "failed" ? "failed" : "";

  return (
    <div className={`history-item ${active ? "active" : ""}`} onClick={onClick} title={item.when}>
      <div className="history-item-line1">
        <span className={`status-dot ${item.status} ${dotClass}`}></span>
        <span className="history-item-name">{item.name}</span>
        {ratio && <span className="history-item-ratio">{ratio}</span>}
        {item.status === "failed" && <span className="history-item-ratio failed">failed</span>}
      </div>
      <div className="history-item-line2">{item.matter !== "—" ? item.matter : "Unfiled"} · {item.when}</div>
    </div>
  );
}

// ============================================================================
// Inline annotation (renders below a paragraph when a finding is selected)
// ============================================================================
function InlineAnnotation({ finding, state, onApply, onAccept, onAddNote, onOpenDetail, onClose }) {
  const [editedSuggestion, setEditedSuggestion] = useState(finding.suggested || "");
  const [noteOpen, setNoteOpen] = useState(false);
  const [noteText, setNoteText] = useState(state?.note || "");
  const [copied, setCopied] = useState(false);
  useEffect(() => {
    setEditedSuggestion(finding.suggested || "");
    setNoteOpen(false);
    setNoteText(state?.note || "");
  }, [finding.id]); // eslint-disable-line

  const copy = () => {
    navigator.clipboard?.writeText(editedSuggestion).catch(() => {});
    setCopied(true);
    setTimeout(() => setCopied(false), 1500);
  };

  const resolution = state?.resolution;
  const sevColor = finding.severity === "high" ? "var(--high)" :
                   finding.severity === "medium" ? "var(--med)" :
                   finding.severity === "info" ? "var(--extra)" : "var(--low)";

  return (
    <aside className={`annotation sev-${finding.severity} ${resolution ? "applied" : ""}`} onClick={e => e.stopPropagation()}>
      <div className="annotation-pointer" style={{borderRightColor: sevColor}}></div>
      <div className="annotation-head">
        <SevPill severity={finding.severity} />
        <StatusPill status={finding.status} />
        <button className="annotation-close" onClick={onClose} aria-label="Close annotation" type="button">
          <Icon.Close />
        </button>
      </div>
      <div className="annotation-title">{finding.title}</div>
      <div className="annotation-summary">{finding.summary}</div>

      {resolution === "replaced" && (
        <div className="annotation-banner">
          <Icon.Check /> Suggestion applied — see green block below the paragraph
        </div>
      )}
      {resolution === "accepted" && (
        <div className="annotation-banner accepted">
          <Icon.Accept /> Risk accepted
        </div>
      )}
      {resolution === "noted" && (
        <div className="annotation-banner noted">
          <Icon.Note /> Note: {state?.note}
        </div>
      )}

      {!resolution && finding.suggested && (
        <>
          <div className="annotation-suggestion-label">
            Suggested {finding.status === "not_found" ? "clause to insert" : finding.status === "extra" ? "response" : "wording"}
            <button className={`copy-btn-tiny ${copied ? "copied" : ""}`} onClick={copy} type="button">
              {copied ? <Icon.Check /> : <Icon.Copy />}
            </button>
          </div>
          <AutoTextarea className="annotation-suggestion"
            value={editedSuggestion}
            onChange={e => setEditedSuggestion(e.target.value)}
          />
        </>
      )}

      {noteOpen && (
        <>
          <div className="annotation-suggestion-label">Add comment</div>
          <AutoTextarea className="annotation-suggestion" value={noteText}
            onChange={e => setNoteText(e.target.value)}
            placeholder="Comment for the team…" />
          <div style={{display: "flex", gap: 6, marginTop: 6, justifyContent: "flex-end"}}>
            <button className="btn btn-sm btn-ghost" onClick={() => setNoteOpen(false)} type="button">Cancel</button>
            <button className="btn btn-sm btn-primary" type="button"
              onClick={() => { onAddNote(noteText); setNoteOpen(false); }}>
              Save
            </button>
          </div>
        </>
      )}

      {!resolution && !noteOpen && (
        <div className="annotation-actions">
          {finding.suggested && (
            <button className="btn btn-sm btn-primary" type="button" onClick={() => onApply(editedSuggestion)}>
              <Icon.Check /> Apply suggestion
            </button>
          )}
          <button className="btn btn-sm" type="button" onClick={() => onAccept()}>
            <Icon.Accept /> Accept
          </button>
          <button className="btn btn-sm" type="button" onClick={() => setNoteOpen(true)}>
            <Icon.Note /> Comment
          </button>
        </div>
      )}
    </aside>
  );
}

// ============================================================================
// Applied suggestion (renders as inline green block under paragraph)
// ============================================================================
function AppliedSuggestion({ finding, onClick, onUndo }) {
  return (
    <div className="applied-suggestion" onClick={onClick} style={{marginLeft: 16}}>
      <div className="applied-head">
        Addition — {finding.title}
        <button className="link-btn" onClick={(e) => { e.stopPropagation(); onUndo(); }} type="button" style={{marginLeft: "auto"}}>
          Undo
        </button>
      </div>
      <div className="applied-body">{finding.suggested}</div>
    </div>
  );
}

// ============================================================================
// Contract viewer (center)
// ============================================================================
function ContractViewer({ data, findingsById, findingsState, selectedFindingId, onSelectFinding, onApply, onAccept, onAddNote, onUnresolve, onOpenDetail, viewMode, onViewModeChange, filterMatch, filterActive, viewerRef }) {
  // Compose a missing-clause anchor right after Section 3 for Termination,
  // and right after Section 12 for Data breach. (Placement is a UX choice:
  // surface the absence near the related area, never in an empty pane.)
  const missingFindings = data.findings.filter(f => f.status === "not_found");

  // Build the list of nodes to render: sections + interleaved missing anchors
  const nodes = [];
  data.contract.sections.forEach((sec) => {
    nodes.push({ kind: "section", section: sec });
    // After Section 3 — insert termination-for-convenience missing anchor
    if (sec.id === "s3") {
      const f = missingFindings.find(x => x.id === "f-termination");
      if (f) nodes.push({ kind: "missing", finding: f });
    }
    // After Section 12 — insert data-breach missing anchor
    if (sec.id === "s12") {
      const f = missingFindings.find(x => x.id === "f-data-breach");
      if (f) nodes.push({ kind: "missing", finding: f });
    }
  });

  // Build per-section maps of marks by paragraph
  function marksByParagraph(section) {
    const map = {};
    (section.marks || []).forEach(m => {
      (map[m.paragraph] ||= []).push(m);
    });
    return map;
  }

  // Active finding info for the toolbar
  const selectedFinding = selectedFindingId ? findingsById[selectedFindingId] : null;
  const findingPositions = useMemo(() => {
    // Build navigation order across all findings that have evidence in the doc
    return data.findings.filter(f => f.status !== "match" && f.status !== "n_a")
                        .map(f => f.id);
  }, [data.findings]);
  const navIdx = selectedFindingId ? findingPositions.indexOf(selectedFindingId) : -1;
  const navPrev = () => {
    if (navIdx > 0) onSelectFinding(findingPositions[navIdx - 1]);
  };
  const navNext = () => {
    if (navIdx >= 0 && navIdx < findingPositions.length - 1) onSelectFinding(findingPositions[navIdx + 1]);
  };

  return (
    <section className={`viewer ${filterActive ? "filter-active" : ""}`}>
      <div className="viewer-toolbar">
        <div className="viewer-search">
          <span className="glass"><Icon.Search /></span>
          <input placeholder="Search in contract…" />
        </div>
        <div className="viewer-toolbar-sep"></div>
        <div className="viewer-toolbar-meta">
          {selectedFinding
            ? <>Issue {navIdx + 1}/{findingPositions.length}: <b style={{color:"var(--ink-2)", fontWeight:600}}>{selectedFinding.title}</b> · {selectedFinding.sectionRef}</>
            : <>{data.contract.sections.length} sections · 12 pages · DOCX</>}
        </div>
        <div className="right">
          <div className="view-toggle">
            <button className={viewMode === "review" ? "on" : ""} onClick={() => onViewModeChange("review")} type="button" title="Review with annotations">
              Review
            </button>
            <button className={viewMode === "final" ? "on" : ""} onClick={() => onViewModeChange("final")} type="button" title="Final view with suggestions applied">
              Final
            </button>
          </div>
          <button className="nav-arrow" onClick={navPrev} disabled={navIdx <= 0} aria-label="Previous issue"><Icon.ArrowLeft /></button>
          <button className="nav-arrow" onClick={navNext} disabled={navIdx < 0 || navIdx >= findingPositions.length - 1} aria-label="Next issue"><Icon.ArrowRight /></button>
        </div>
      </div>
      <div className="viewer-body" ref={viewerRef}>
        <article className="contract">
          <header className="contract-header">
            <div className="meta">Master Services Agreement</div>
            <h1>between {data.contract.party} and {data.contract.counterparty}</h1>
            <div className="parties">Effective {data.contract.effectiveDate}</div>
          </header>

          {nodes.map((node, i) => {
            if (node.kind === "section") {
              const sec = node.section;
              const marks = marksByParagraph(sec);
              const hasFinding = (sec.marks || []).some(m => m.findingId);
              const associatedFinding = hasFinding ? findingsById[sec.marks[0].findingId] : null;
              return (
                <div className="section" key={sec.id} id={`sec-${sec.id}`} data-screen-label={`§${sec.number}`}>
                  <h3 className="section-title">
                    <span className="section-num">{sec.number}.</span>
                    {sec.title}
                  </h3>
                  {sec.paragraphs.map((para, pi) => {
                    const paraMarks = marks[pi] || [];
                    const selectedOnPara = paraMarks.some(m => m.findingId === selectedFindingId);
                    const selectedF = (viewMode === "review" && selectedOnPara) ? findingsById[selectedFindingId] : null;
                    const appliedOnPara = viewMode === "review" ? paraMarks
                      .map(m => findingsById[m.findingId])
                      .filter(f => f && findingsState?.[f.id]?.resolution === "replaced"
                              && f.id === selectedFindingId
                              && (!filterMatch || filterMatch(f))) : [];
                    return (
                      <React.Fragment key={pi}>
                        <p>
                          {renderParagraph(para, paraMarks, selectedFindingId, onSelectFinding, findingsById, findingsState, viewMode, filterMatch)}
                        </p>
                        {appliedOnPara.map(f => (
                          <AppliedSuggestion
                            key={`applied-${f.id}`}
                            finding={f}
                            onClick={() => onSelectFinding(f.id)}
                            onUndo={() => onUnresolve(f.id)}
                          />
                        ))}
                        {selectedF && !findingsState?.[selectedF.id]?.resolution && (
                          <InlineAnnotation
                            finding={selectedF}
                            state={findingsState?.[selectedF.id]}
                            onApply={() => onApply(selectedF.id)}
                            onAccept={() => onAccept(selectedF.id)}
                            onAddNote={(note) => onAddNote(selectedF.id, note)}
                            onOpenDetail={() => onOpenDetail(selectedF.id)}
                            onClose={() => onSelectFinding(null)}
                          />
                        )}
                      </React.Fragment>
                    );
                  })}
                </div>
              );
            }
            if (node.kind === "missing") {
              const f = node.finding;
              const isSelected = selectedFindingId === f.id;
              const fState = findingsState && findingsState[f.id];
              const inserted = fState?.resolution === "replaced";
              const accepted = fState?.resolution === "accepted";
              const matchesFilter = !filterMatch || filterMatch(f);
              if (viewMode === "final" && !inserted) return null;
              return (
                <React.Fragment key={`m-${f.id}`}>
                  {inserted && f.suggested ? (
                    <div className={`inserted-clause ${isSelected ? "selected" : ""} ${viewMode === "final" ? "final" : ""} ${matchesFilter ? "" : "dimmed"}`}
                      id={`miss-${f.id}`}
                      onClick={() => onSelectFinding(f.id)}>
                      {viewMode === "review" && (
                        <div className="label">
                          <Icon.Plus /> Inserted from playbook · {f.title}
                          <button className="link-btn" style={{marginLeft: "auto"}}
                            onClick={(e) => { e.stopPropagation(); onUnresolve(f.id); }} type="button">
                            Undo
                          </button>
                        </div>
                      )}
                      <div className="ins-body">{f.suggested}</div>
                    </div>
                  ) : (
                    <div
                      className={`missing-anchor ${isSelected ? "selected" : ""} ${accepted ? "accepted" : ""} ${matchesFilter ? "" : "dimmed"}`}
                      id={`miss-${f.id}`}
                      onClick={() => onSelectFinding(f.id)}
                    >
                      <div className="icon">{accepted ? "✓" : "∅"}</div>
                      <div className="body">
                        <div className="label">
                          {accepted ? "Risk accepted · clause missing" : "Required clause not found"} · {SEV_LABEL[f.severity]} risk
                        </div>
                        <div className="title">{f.title}</div>
                        <div className="desc">{f.summary}</div>
                      </div>
                    </div>
                  )}
                  {isSelected && viewMode === "review" && !fState?.resolution && (
                    <InlineAnnotation
                      finding={f}
                      state={fState}
                      onApply={() => onApply(f.id)}
                      onAccept={() => onAccept(f.id)}
                      onAddNote={(note) => onAddNote(f.id, note)}
                      onOpenDetail={() => onOpenDetail(f.id)}
                      onClose={() => onSelectFinding(null)}
                    />
                  )}
                </React.Fragment>
              );
            }
            return null;
          })}

          <div style={{
            marginTop: 32,
            paddingTop: 20,
            borderTop: "1px solid var(--border-soft)",
            fontFamily: "var(--mono)",
            fontSize: 11,
            color: "var(--ink-muted)",
            textAlign: "center",
            letterSpacing: "0.04em"
          }}>
            END OF DOCUMENT · 12 PAGES · ACME_CLOUD_SYSTEMS_MSA_V3.DOCX
          </div>
        </article>
      </div>
    </section>
  );
}

// ============================================================================
// Right review panel (summary + filters + list)
// ============================================================================
function ReviewPanel({ data, filters, setFilters, selectedFindingId, onSelectFinding, findingsState, selectedFinding, onCopy, onJumpTo, onAccept, onReplace, onAddNote, onUnresolve, onCopyLink, onAddToExport, onMore }) {
  const c = data.counts;

  // Combined filter set. severityStatus key examples: 'high' | 'medium' | 'low' | 'not_found' | 'extra'
  // Default = empty set = "all deviations" (everything except matches+n/a)
  const active = filters.cats; // Set<string>
  const resolutionFilter = filters.resolution; // 'unresolved' | 'all' | 'resolved'
  const showMatches = filters.showMatches; // bool

  const matchesFinding = (f) => {
    if (showMatches) return f.status === "match"; // matches view
    // Default: deviations only — exclude match + n/a
    if (f.status === "match" || f.status === "n_a") return false;
    if (active.size === 0) return true;
    // Category filters: high / medium / low / not_found / extra
    if (active.has("high") && f.severity === "high") return true;
    if (active.has("medium") && f.severity === "medium") return true;
    if (active.has("low") && f.severity === "low") return true;
    if (active.has("not_found") && f.status === "not_found") return true;
    if (active.has("extra") && f.status === "extra") return true;
    return false;
  };

  const filteredFindings = useMemo(() => {
    let list = data.findings.filter(matchesFinding);
    // Apply resolution filter
    if (!showMatches) {
      if (resolutionFilter === "unresolved") list = list.filter(f => (findingsState[f.id]?.resolution || "unresolved") === "unresolved");
      else if (resolutionFilter === "resolved") list = list.filter(f => (findingsState[f.id]?.resolution || "unresolved") !== "unresolved");
    }
    const sevOrder = { high: 0, medium: 1, low: 2, info: 3 };
    const statOrder = { does_not_match: 0, not_found: 1, partial: 2, needs_review: 3, extra: 4, match: 5, n_a: 6 };
    return list.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity] || statOrder[a.status] - statOrder[b.status]);
  }, [data.findings, active, resolutionFilter, showMatches, findingsState]);

  const toggleCat = (cat) => {
    const next = new Set(active);
    next.has(cat) ? next.delete(cat) : next.add(cat);
    setFilters({ ...filters, cats: next, showMatches: false });
  };
  const clearCats = () => setFilters({ ...filters, cats: new Set(), showMatches: false });
  const showOnlyMatches = () => setFilters({ ...filters, cats: new Set(), showMatches: true });

  // Resolved counts
  const resolvedCount = data.findings.filter(f =>
    f.status !== "match" && f.status !== "n_a" &&
    (findingsState[f.id]?.resolution || "unresolved") !== "unresolved"
  ).length;
  const unresolvedCount = data.findings.filter(f =>
    f.status !== "match" && f.status !== "n_a" &&
    (findingsState[f.id]?.resolution || "unresolved") === "unresolved"
  ).length;

  // Width fractions for risk bar
  const attnTotal = c.high + c.medium + c.low + c.extra;
  const w = {
    high:  (c.high / attnTotal) * 100,
    med:   (c.medium / attnTotal) * 100,
    low:   (c.low / attnTotal) * 100,
    extra: (c.extra / attnTotal) * 100
  };

  return (
    <aside className="review">
      <div className="review-summary">
        <div className="review-summary-header">
          <span className="review-summary-eyebrow">Review status</span>
          <span className="review-summary-eyebrow" style={{color:"var(--ink-soft)"}}>v{data.playbook.version.replace("v","")} snapshot</span>
        </div>
        <div className="review-summary-verdict">
          <span className="verdict-dot"></span>
          Needs legal review
        </div>
        <div className="risk-bar" title="Severity distribution" style={{marginTop: 10}}>
          <div style={{ width: `${w.high}%`, background: "var(--high)" }}></div>
          <div style={{ width: `${w.med}%`,  background: "var(--med)" }}></div>
          <div style={{ width: `${w.low}%`,  background: "var(--low)" }}></div>
          <div style={{ width: `${w.extra}%`, background: "var(--extra)" }}></div>
        </div>

        <div className="filter-counters">
          <button className={`fc high ${active.has("high") ? "on" : ""}`} onClick={() => toggleCat("high")} type="button">
            <span className="fc-num">{c.high}</span>
            <span className="fc-lbl">High</span>
          </button>
          <button className={`fc med ${active.has("medium") ? "on" : ""}`} onClick={() => toggleCat("medium")} type="button">
            <span className="fc-num">{c.medium}</span>
            <span className="fc-lbl">Medium</span>
          </button>
          <button className={`fc miss ${active.has("not_found") ? "on" : ""}`} onClick={() => toggleCat("not_found")} type="button">
            <span className="fc-num">{c.not_found}</span>
            <span className="fc-lbl">Not found</span>
          </button>
          <button className={`fc extra ${active.has("extra") ? "on" : ""}`} onClick={() => toggleCat("extra")} type="button">
            <span className="fc-num">{c.extra}</span>
            <span className="fc-lbl">Extra</span>
          </button>
        </div>

        {(showMatches || active.size > 0) && (
          <div className="filter-meta">
            {showMatches ? (
              <>Showing matches · <button className="link-btn" onClick={clearCats} type="button">Back to deviations</button></>
            ) : (
              <>Filtered to <b>{active.size}</b> {active.size === 1 ? "category" : "categories"} · <button className="link-btn" onClick={clearCats} type="button">Clear</button></>
            )}
          </div>
        )}
      </div>

      <div className="review-filters">
        <div className="res-row">
          <button className={`res ${resolutionFilter === "unresolved" ? "on" : ""}`}
            onClick={() => setFilters({...filters, resolution: "unresolved"})} type="button">
            <span className="res-dot unresolved"></span>
            Unresolved <span className="res-count">{unresolvedCount}</span>
          </button>
          <button className={`res ${resolutionFilter === "all" ? "on" : ""}`}
            onClick={() => setFilters({...filters, resolution: "all"})} type="button">
            All <span className="res-count">{unresolvedCount + resolvedCount}</span>
          </button>
          <button className={`res ${resolutionFilter === "resolved" ? "on" : ""}`}
            onClick={() => setFilters({...filters, resolution: "resolved"})} type="button">
            <span className="res-dot resolved"></span>
            Resolved <span className="res-count">{resolvedCount}</span>
          </button>
        </div>
      </div>

      {selectedFinding ? (
        <FindingDetail
          finding={selectedFinding}
          state={findingsState[selectedFinding.id]}
          onBack={() => onSelectFinding(null)}
          onCopy={onCopy}
          onJumpTo={onJumpTo}
          onAccept={onAccept}
          onReplace={onReplace}
          onAddNote={onAddNote}
          onUnresolve={onUnresolve}
          onCopyLink={onCopyLink}
          onAddToExport={onAddToExport}
          onMore={onMore}
        />
      ) : (
        <div className="review-list">
          {showMatches ? (
            <EmptyMatches count={c.match} />
          ) : filteredFindings.length === 0 ? (
            <EmptyResults onReset={() => setFilters({ ...filters, cats: new Set(), resolution: "all" })} />
          ) : (
            filteredFindings.map(f => (
              <FindingCard
                key={f.id}
                finding={f}
                state={findingsState[f.id]}
                selected={selectedFindingId === f.id}
                onClick={() => onSelectFinding(f.id)}
              />
            ))
          )}
        </div>
      )}
    </aside>
  );
}

function FindingCard({ finding, state, selected, onClick }) {
  const resolution = state?.resolution;
  return (
    <div className={`finding-card ${selected ? "selected" : ""} ${resolution ? "resolved" : ""}`} onClick={onClick}>
      <div className="finding-meta">
        <SevPill severity={finding.severity} />
        <StatusPill status={finding.status} compact />
        <span style={{marginLeft:"auto"}}>{finding.area}</span>
      </div>
      <div className="finding-title">{finding.title}</div>
      <div className="finding-summary">{finding.summary}</div>
      <div className="finding-footer">
        <span className="ref">{finding.sectionRef || "— No source —"}</span>
        {resolution && (
          <span className={`resolution-tag ${resolution}`}>
            {resolution === "accepted" && <><Icon.Accept /> Accepted</>}
            {resolution === "replaced" && <><Icon.Replace /> Replaced</>}
            {resolution === "noted" && <><Icon.Note /> Noted</>}
          </span>
        )}
        {!resolution && <a className="open-link" href="#">Open →</a>}
      </div>
    </div>
  );
}

function EmptyResults({ onReset }) {
  return (
    <div style={{padding:"40px 16px", textAlign:"center", color:"var(--ink-soft)"}}>
      <div style={{fontSize: 14, fontWeight: 500, color: "var(--ink)", marginBottom: 6}}>No findings match your filters</div>
      <div style={{fontSize: 12.5, marginBottom: 12}}>Try clearing severity or status chips.</div>
      <button className="btn btn-sm" onClick={onReset} type="button">Reset filters</button>
    </div>
  );
}
function EmptyMatches({ count }) {
  return (
    <div style={{padding:"32px 14px"}}>
      <div className="status-pill match" style={{marginBottom: 10}}>
        <span className="glyph"></span>{count} matches
      </div>
      <div style={{fontSize: 13, color: "var(--ink-soft)", lineHeight: 1.55, marginBottom: 14}}>
        These criteria meet the firm position. They are hidden from the default review to keep focus on deviations.
      </div>
      <div style={{display:"flex", flexDirection:"column", gap: 6}}>
        {["Definitions consistent with playbook", "Insurance limits acceptable",
          "Force Majeure clause acceptable", "Assignment restrictions acceptable",
          "Severability standard", "Entire agreement clause present",
          "Counterparts clause present", "+ 23 more matches"].map((s, i) => (
          <div key={i} style={{
            padding:"7px 10px",
            background:"var(--paper-doc)",
            border:"1px solid var(--border-soft)",
            borderRadius: 4,
            fontSize: 12.5,
            color: "var(--ink-2)",
            display: "flex", alignItems: "center", gap: 8
          }}>
            <span style={{width: 6, height: 6, borderRadius: "50%", background: "var(--match)"}}></span>
            {s}
          </div>
        ))}
      </div>
    </div>
  );
}

// ============================================================================
// Finding Detail (slides over the review panel)
// ============================================================================
function FindingDetail({ finding, state, onBack, onCopy, onJumpTo, onAccept, onReplace, onAddNote, onUnresolve, onCopyLink, onAddToExport, onMore }) {
  const [editedSuggestion, setEditedSuggestion] = useState(finding.suggested || "");
  const [copied, setCopied] = useState(false);
  const [noteOpen, setNoteOpen] = useState(false);
  const [noteText, setNoteText] = useState(state?.note || "");
  const [acceptOpen, setAcceptOpen] = useState(false);
  const [acceptReason, setAcceptReason] = useState(state?.acceptReason || "");

  useEffect(() => {
    setEditedSuggestion(finding.suggested || "");
    setCopied(false);
    setNoteOpen(false);
    setAcceptOpen(false);
    setNoteText(state?.note || "");
    setAcceptReason(state?.acceptReason || "");
  }, [finding.id]); // eslint-disable-line

  const copySuggestion = () => {
    navigator.clipboard?.writeText(editedSuggestion).catch(() => {});
    setCopied(true);
    onCopy && onCopy("Suggested wording copied");
    setTimeout(() => setCopied(false), 1800);
  };

  const isExtra = finding.status === "extra";
  const isMissing = finding.status === "not_found";
  const resolution = state?.resolution;

  return (
    <div className="detail">
      <button className="detail-back" onClick={onBack} type="button">
        <Icon.ArrowLeft /> Back to findings
      </button>
      <div className="detail-head">
        <div className="detail-pills">
          <SevPill severity={finding.severity} />
          <StatusPill status={finding.status} />
          <span className="status-pill" style={{
            background: "var(--accent-soft)", borderColor: "transparent", color: "var(--accent)"
          }}>
            <Icon.Folder /> {finding.area}
          </span>
        </div>
        <h3 className="detail-title">{finding.title}</h3>
        <div className="detail-section-ref">
          {finding.sectionId ? (
            <>Evidence in <a onClick={() => onJumpTo(finding.sectionId)}>{finding.sectionRef}</a></>
          ) : (
            <>Clause not present in incoming contract</>
          )}
        </div>
        {resolution && (
          <div className={`resolution-banner ${resolution}`}>
            {resolution === "accepted" && <><Icon.Accept /> Risk accepted</>}
            {resolution === "replaced" && <><Icon.Replace /> Replaced in contract</>}
            {resolution === "noted" && <><Icon.Note /> Note added</>}
            <button className="link-btn" onClick={onUnresolve} type="button" style={{marginLeft: "auto"}}>Reopen</button>
          </div>
        )}
      </div>

      <div className="detail-body">
        <div className="detail-block">
          <p className="detail-block-label">
            {isMissing ? "What we looked for, but did not find" :
             isExtra   ? "Found in contract (no playbook standard)" :
                         "Found in incoming contract"}
          </p>
          {finding.foundQuote ? (
            <div className={`quote ${finding.severity === "high" ? "high" : ""} ${isExtra ? "extra" : ""}`}>
              “{finding.foundQuote}”
            </div>
          ) : (
            <div className="quote" style={{
              borderLeftColor: "var(--nf)",
              fontStyle: "normal",
              fontFamily: "var(--ui)",
              background: "var(--nf-bg)",
              color: "var(--nf)",
              fontSize: 13
            }}>
              No matching clause was found in the contract text.
            </div>
          )}
          {finding.candidates && finding.candidates.length > 0 && (
            <div className="candidates">
              <div className="detail-block-label" style={{marginBottom: 6}}>Possible related sections</div>
              <div className="candidates-row">
                {finding.candidates.map(c => <span className="candidate-chip" key={c}>{c}</span>)}
              </div>
            </div>
          )}
        </div>

        <div className="detail-block">
          <p className="detail-block-label">{isExtra ? "Firm context" : "Your standard position"}</p>
          <p>{finding.firmPosition}</p>
        </div>

        <div className="detail-block">
          <p className="detail-block-label">Why this matters</p>
          <p>{finding.whyItMatters}</p>
          {finding.conflicts && finding.conflicts.length > 0 && (
            <ul style={{ margin: "6px 0 0", paddingLeft: 18, fontSize: 13, color: "var(--ink-2)", lineHeight: 1.55 }}>
              {finding.conflicts.map((c, i) => <li key={i}>{c}</li>)}
            </ul>
          )}
        </div>

        <div className="detail-block">
          <p className="detail-block-label">
            {isMissing ? "Suggested clause to insert" :
             isExtra ? "Suggested response" :
             "Suggested wording"}
          </p>
          <div className="suggested-wrap">
            <div className="suggested-toolbar">
              <span>Edit before applying</span>
              <button className={`copy-btn ${copied ? "copied" : ""}`} onClick={copySuggestion} type="button">
                {copied ? <><Icon.Check /> Copied</> : <><Icon.Copy /> Copy</>}
              </button>
            </div>
            <AutoTextarea className="suggested-text"
              value={editedSuggestion}
              onChange={e => setEditedSuggestion(e.target.value)}
            />
          </div>
        </div>

        {noteOpen && (
          <div className="detail-block">
            <p className="detail-block-label">Add note</p>
            <textarea className="input" rows="3" value={noteText} onChange={e => setNoteText(e.target.value)}
              placeholder="Optional context for the team or yourself…" autoFocus></textarea>
            <div style={{display:"flex", gap: 6, marginTop: 8, justifyContent: "flex-end"}}>
              <button className="btn btn-sm btn-ghost" onClick={() => setNoteOpen(false)} type="button">Cancel</button>
              <button className="btn btn-sm btn-primary" type="button"
                onClick={() => { onAddNote(noteText); setNoteOpen(false); }}>
                Save note
              </button>
            </div>
          </div>
        )}
        {acceptOpen && (
          <div className="detail-block">
            <p className="detail-block-label">Accept this risk</p>
            <textarea className="input" rows="3" value={acceptReason} onChange={e => setAcceptReason(e.target.value)}
              placeholder="Reason for accepting (optional — e.g. ‘Acceptable for this counterparty given matter risk profile’)" autoFocus></textarea>
            <div style={{display:"flex", gap: 6, marginTop: 8, justifyContent: "flex-end"}}>
              <button className="btn btn-sm btn-ghost" onClick={() => setAcceptOpen(false)} type="button">Cancel</button>
              <button className="btn btn-sm btn-primary" type="button"
                onClick={() => { onAccept(acceptReason); setAcceptOpen(false); }}>
                <Icon.Accept /> Confirm accept
              </button>
            </div>
          </div>
        )}
      </div>

      <div className="detail-actions">
        {!resolution && !noteOpen && !acceptOpen && (
          <>
            {!isMissing && (
              <button className="btn btn-primary action-replace" type="button" onClick={onReplace}>
                <Icon.Replace />
                {isExtra ? "Replace with firm wording" : "Replace in contract"}
              </button>
            )}
            {isMissing && (
              <button className="btn btn-primary action-replace" type="button" onClick={onReplace}>
                <Icon.Plus />
                Insert clause into contract
              </button>
            )}
            <button className="btn" type="button" onClick={() => setAcceptOpen(true)}>
              <Icon.Accept /> Accept risk
            </button>
            <button className="btn" type="button" onClick={() => setNoteOpen(true)}>
              <Icon.Note /> Add note
            </button>
            <button className="btn btn-ghost btn-icon" type="button"
              onClick={onMore} title="More options">
              <Icon.More />
            </button>
          </>
        )}
        {resolution && (
          <button className="btn" type="button" onClick={onUnresolve}>
            <Icon.Refresh /> Reopen
          </button>
        )}
      </div>
    </div>
  );
}

// ============================================================================
// Drawers: Upload + Playbook
// ============================================================================
function UploadDrawer({ data, onClose, onAnalyzeFile, onAnalyzeText, onImportGoogleDoc, onSaveApiKey }) {
  const [file, setFile] = useState(null);
  const [text, setText] = useState("");
  const [googleUrl, setGoogleUrl] = useState("");
  const [apiKey, setApiKey] = useState(getApiKey());
  const [busy, setBusy] = useState(false);
  const run = async (fn) => {
    setBusy(true);
    try {
      await fn();
      onClose();
    } finally {
      setBusy(false);
    }
  };

  return (
    <>
      <div className="scrim" onClick={onClose}></div>
      <div className="drawer" role="dialog" aria-label="Analyze new contract">
        <div className="drawer-head">
          <h2>Analyze new contract</h2>
          <button className="close" onClick={onClose} aria-label="Close"><Icon.Close /></button>
        </div>
        <div className="drawer-body">
          <div className="form-group">
            <div className="form-label">API key (optional)</div>
            <input
              id="apiKey"
              className="input"
              value={apiKey}
              onChange={(event) => setApiKey(event.target.value)}
              onBlur={() => onSaveApiKey(apiKey)}
              placeholder="Used only when this workspace requires X-API-Key"
            />
          </div>

          <div className="form-group">
            <div className="form-label">1 · Choose playbook</div>
            <button className="playbook-pill" type="button" style={{padding: "8px 12px", height: 38, fontSize: 13}}>
              <span className="dot"></span>
              <span className="label">active</span>
              <span className="name">{data.playbook.name}</span>
              <span className="caret"><Icon.ChevronDown /></span>
            </button>
          </div>

          <div className="form-group">
            <div className="form-label">2 · Add contract</div>
            <label className="dropzone" htmlFor="incomingFile">
              <div className="big">Drop a file here, or click to browse</div>
              <div className="small">{file ? file.name : "DOCX · DOC · structured PDF · MD · TXT — up to 25 MB"}</div>
              <input
                id="incomingFile"
                type="file"
                accept=".pdf,.docx,.doc,.txt,.md,text/plain,text/markdown"
                style={{display: "none"}}
                onChange={(event) => setFile(event.target.files?.[0] || null)}
              />
            </label>
            <div className="format-row">
              <span className="format-tag">DOCX</span>
              <span className="format-tag">DOC</span>
              <span className="format-tag">PDF (text)</span>
              <span className="format-tag">MD</span>
              <span className="format-tag">TXT</span>
              <span className="format-tag no">Scanned PDF</span>
              <span className="format-tag no">Image</span>
            </div>
          </div>

          <div style={{display: "flex", alignItems: "center", gap: 12, margin: "20px 0 8px"}}>
            <div style={{flex: 1, height: 1, background: "var(--border-soft)"}}></div>
            <div style={{fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-muted)", letterSpacing: "0.04em"}}>OR</div>
            <div style={{flex: 1, height: 1, background: "var(--border-soft)"}}></div>
          </div>

          <div className="form-group">
            <div className="form-label">Paste contract text</div>
            <textarea
              id="incomingText"
              className="input"
              rows="5"
              value={text}
              onChange={(event) => setText(event.target.value)}
              placeholder="Paste contract text directly. We will preserve section structure."
            ></textarea>
          </div>

          <div className="form-group">
            <div className="form-label">Import from Google Doc</div>
            <input
              id="googleDocUrl"
              className="input"
              value={googleUrl}
              onChange={(event) => setGoogleUrl(event.target.value)}
              placeholder="https://docs.google.com/document/d/..."
            />
            <div style={{fontSize: 11.5, color: "var(--ink-muted)", marginTop: 4}}>Must be a publicly viewable link.</div>
          </div>

          <div className="form-group">
            <div className="form-label">Matter (optional)</div>
            <input className="input" placeholder="M-2026-XXX · Client name" />
          </div>
        </div>
        <div className="drawer-foot">
          <button className="btn btn-ghost" onClick={onClose} type="button">Cancel</button>
          <button
            id="pasteIncoming"
            className="btn"
            type="button"
            disabled={busy || !text.trim()}
            onClick={() => run(() => onAnalyzeText(text))}
          >
            Paste text
          </button>
          <button
            id="importGoogleDoc"
            className="btn"
            type="button"
            disabled={busy || !googleUrl.trim()}
            onClick={() => run(() => onImportGoogleDoc(googleUrl))}
          >
            Import link
          </button>
          <button
            id="uploadIncoming"
            className="btn btn-primary"
            type="button"
            disabled={busy || !file}
            onClick={() => run(() => onAnalyzeFile(file))}
          >
            <Icon.Sparkle /> {busy ? "Starting…" : "Start analysis"}
          </button>
        </div>
      </div>
    </>
  );
}

function ExportDrawer({ data, findingsState, onClose, onCopy }) {
  const [includeMatches, setIncludeMatches] = useState(false);
  const [includeMatter, setIncludeMatter] = useState(true);
  const [includeSuggested, setIncludeSuggested] = useState(true);
  const [includeNotes, setIncludeNotes] = useState(true);
  const [includeRedline, setIncludeRedline] = useState(true);
  const [audience, setAudience] = useState("internal");
  const [format, setFormat] = useState("docx");

  const sevCount = (sev) => data.findings.filter(f => f.severity === sev).length;
  const replacedCount = Object.values(findingsState).filter(s => s?.resolution === "replaced").length;
  const acceptedCount = Object.values(findingsState).filter(s => s?.resolution === "accepted").length;

  return (
    <>
      <div className="scrim" onClick={onClose}></div>
      <div className="drawer wide" role="dialog" aria-label="Export report">
        <div className="drawer-head">
          <h2>Export report</h2>
          <span className="ext-meta">{data.contract.filename}</span>
          <button className="close" onClick={onClose} aria-label="Close"><Icon.Close /></button>
        </div>
        <div className="drawer-body">
          <div className="export-layout">
            <div className="export-options">
              <div className="form-group">
                <div className="form-label">Audience</div>
                <div className="seg-toggle">
                  <button className={audience === "internal" ? "on" : ""} onClick={() => setAudience("internal")} type="button">
                    Internal legal team
                  </button>
                  <button className={audience === "client" ? "on" : ""} onClick={() => setAudience("client")} type="button">
                    Client-facing summary
                  </button>
                </div>
                {audience === "client" ? (
                  <div className="form-hint">Hides AI reasoning, confidence scores, model notes, and internal team comments. Keeps the issue, what was found, and the suggested wording — formatted for sharing with the counterparty or business stakeholder.</div>
                ) : (
                  <div className="form-hint">Full review pack with model reasoning, confidence, source quality, and team notes. Use when handing off to another lawyer on the matter.</div>
                )}
              </div>

              <div className="form-group">
                <div className="form-label">Format</div>
                <div className="seg-toggle">
                  <button className={format === "docx" ? "on" : ""} onClick={() => setFormat("docx")} type="button">DOCX report</button>
                  <button className={format === "redline" ? "on" : ""} onClick={() => setFormat("redline")} type="button">Redlined contract</button>
                  <button className={format === "csv" ? "on" : ""} onClick={() => setFormat("csv")} type="button">CSV checklist</button>
                </div>
              </div>

              <div className="form-group">
                <div className="form-label">Include</div>
                <label className="check-row">
                  <input type="checkbox" checked readOnly />
                  <span>High-risk findings <span className="muted">({sevCount("high")})</span></span>
                </label>
                <label className="check-row">
                  <input type="checkbox" checked readOnly />
                  <span>Medium-risk findings <span className="muted">({sevCount("medium")})</span></span>
                </label>
                <label className="check-row">
                  <input type="checkbox" checked readOnly />
                  <span>Missing clauses <span className="muted">({data.counts.not_found})</span></span>
                </label>
                <label className="check-row">
                  <input type="checkbox" checked readOnly />
                  <span>Extra clauses <span className="muted">({data.counts.extra})</span></span>
                </label>
                <label className="check-row">
                  <input type="checkbox" checked={includeSuggested} onChange={e => setIncludeSuggested(e.target.checked)} />
                  <span>Suggested wording / redlines</span>
                </label>
                <label className="check-row">
                  <input type="checkbox" checked={includeNotes} onChange={e => setIncludeNotes(e.target.checked)} />
                  <span>Internal notes <span className="muted">({Object.values(findingsState).filter(s => s?.note).length})</span></span>
                </label>
                <label className="check-row">
                  <input type="checkbox" checked={includeMatches} onChange={e => setIncludeMatches(e.target.checked)} />
                  <span>Matching checks <span className="muted">({data.counts.match})</span></span>
                </label>
                <label className="check-row">
                  <input type="checkbox" checked={includeMatter} onChange={e => setIncludeMatter(e.target.checked)} />
                  <span>Matter / client metadata</span>
                </label>
                {format === "redline" && (
                  <label className="check-row">
                    <input type="checkbox" checked={includeRedline} onChange={e => setIncludeRedline(e.target.checked)} />
                    <span>Show resolved replacements as track-changes</span>
                  </label>
                )}
              </div>
            </div>

            <div className="export-preview">
              <div className="export-preview-label">Preview</div>
              <div className="export-page">
                <div className="ep-corner">Legal · M-2026-118</div>
                <div className="ep-title">{format === "redline" ? "Acme MSA — Redlined" : "Contract Review Report"}</div>
                <div className="ep-meta">{data.contract.filename}</div>
                {audience === "internal" && includeMatter && (
                  <div className="ep-meta">{data.contract.counterparty} · {data.playbook.name} {data.playbook.version}</div>
                )}
                <hr className="ep-rule" />
                {format === "docx" && (
                  <>
                    <div className="ep-section">
                      <div className="ep-h">Summary</div>
                      <div className="ep-row"><span className="ep-bullet" style={{background: "var(--high)"}}></span>3 high-risk deviations</div>
                      <div className="ep-row"><span className="ep-bullet" style={{background: "var(--med)"}}></span>5 medium-risk deviations</div>
                      <div className="ep-row"><span className="ep-bullet" style={{background: "transparent", border: "1.2px dashed var(--nf)"}}></span>2 required clauses not found</div>
                      <div className="ep-row"><span className="ep-bullet" style={{background: "var(--extra)"}}></span>2 extras outside playbook</div>
                      {(replacedCount + acceptedCount > 0) && (
                        <div className="ep-row" style={{marginTop: 6}}><span className="ep-bullet" style={{background: "var(--match)"}}></span>{replacedCount} replaced, {acceptedCount} accepted by reviewer</div>
                      )}
                    </div>
                    <div className="ep-section">
                      <div className="ep-h">Issue 1 of 12 — Liability cap (High · Section 11.2)</div>
                      <div className="ep-quote">
                        <span style={{textDecoration: "line-through", color: "var(--ink-muted)"}}>"Supplier's total cumulative liability... twelve (12) months..."</span><br/>
                        <span style={{background: "var(--match-bg)", padding: "1px 3px"}}>"Notwithstanding the foregoing, the limitations in this Section 11 shall not apply to confidentiality, IP, data breach..."</span>
                      </div>
                      {audience === "internal" && (
                        <div className="ep-small">Why it matters: 12-month cap with no carve-outs; firm standard requires fees-paid + carve-outs.</div>
                      )}
                    </div>
                    <div className="ep-section" style={{opacity: 0.5}}>
                      <div className="ep-h">Issue 2 of 12 — Indemnification scope...</div>
                      <div className="ep-quote ep-muted">[...]</div>
                    </div>
                  </>
                )}
                {format === "redline" && (
                  <>
                    <div className="ep-section">
                      <div className="ep-h">11. Limitation of Liability</div>
                      <div className="ep-quote">
                        EXCEPT FOR LIABILITY ARISING FROM A PARTY'S GROSS NEGLIGENCE...<br/>
                        <span style={{textDecoration: "line-through", color: "var(--high)", background: "var(--high-bg)"}}>Supplier's total cumulative liability... shall not exceed the total fees paid by Customer to Supplier during the twelve (12) months immediately preceding the event giving rise to the claim.</span>
                        {" "}
                        <span style={{background: "var(--match-bg)", color: "var(--match)", padding: "0 2px"}}>Notwithstanding the foregoing, the limitations in this Section 11 shall not apply to (a) either party's indemnification obligations; (b) breach of Section 12 (Confidentiality)...</span>
                      </div>
                      <div className="ep-small">[1 change · L. Voss · today]</div>
                    </div>
                    <div className="ep-section">
                      <div className="ep-h">12. Confidentiality</div>
                      <div className="ep-quote ep-muted">[3 sub-changes pending]</div>
                    </div>
                  </>
                )}
                {format === "csv" && (
                  <pre className="ep-code">{`# id,severity,status,area,section,title,resolution
f-liability,high,does_not_match,Liability,11.2,Liability cap,replaced
f-termination,high,not_found,Termination,—,Customer termination for convenience,unresolved
f-indemnity,high,does_not_match,Indemnity,13.1,Indemnification scope,unresolved
f-payment,medium,partial,Payment,4.2,Late fee cap,unresolved
...`}</pre>
                )}
              </div>
            </div>
          </div>
        </div>
        <div className="drawer-foot">
          <button className="btn btn-ghost" onClick={onClose} type="button">Cancel</button>
          <button className="btn" type="button" onClick={() => onCopy("Copied as plain text")}><Icon.Copy /> Copy as text</button>
          <button className="btn btn-primary" type="button" onClick={() => { onCopy("Downloading " + format.toUpperCase() + "…"); onClose(); }}>
            <Icon.Download /> Download {format.toUpperCase()}
          </button>
        </div>
      </div>
    </>
  );
}

function TemplatesDrawer({ data, onClose, onEditTemplate, onRunWithTemplate, onSetActive }) {
  const [running, setRunning] = useState(null);
  const handleRun = (t) => {
    setRunning(t.id);
    setTimeout(() => {
      setRunning(null);
      onRunWithTemplate(t.id);
    }, 1200);
  };
  return (
    <>
      <div className="scrim" onClick={onClose}></div>
      <div className="drawer" role="dialog" aria-label="Templates">
        <div className="drawer-head">
          <h2>Templates</h2>
          <button className="close" onClick={onClose} aria-label="Close"><Icon.Close /></button>
        </div>
        <div className="drawer-body">
          <div className="tmpl-context">
            <div className="form-label" style={{marginBottom: 4}}>Current contract</div>
            <div style={{fontWeight: 500, fontSize: 14}}>{data.contract.filename}</div>
            <div style={{fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--ink-muted)", marginTop: 2}}>
              M-2026-118 · Acme onboarding
            </div>
          </div>

          <div className="tmpl-section-label">
            <span>Run this contract through</span>
            <span style={{color: "var(--ink-muted)"}}>{data.templates.length} templates</span>
          </div>

          <div className="tmpl-list">
            {data.templates.map(t => {
              const isRunning = running === t.id;
              return (
                <div key={t.id} className={`tmpl-card ${t.active ? "active" : ""}`}>
                  <div className="tmpl-card-head">
                    <span className={`tmpl-radio ${t.active ? "on" : ""}`}>
                      {t.active && <span className="dot"></span>}
                    </span>
                    <div style={{flex: 1, minWidth: 0}}>
                      <div className="tmpl-card-name">
                        {t.name}
                        {t.active && <span className="tmpl-active-tag">Active</span>}
                      </div>
                      <div className="tmpl-card-meta">
                        {t.criteriaCount} criteria · {t.version} · {t.lastEdited}
                      </div>
                    </div>
                    <button className="btn btn-sm btn-ghost"
                      onClick={(e) => { e.stopPropagation(); onEditTemplate(t.id); }}
                      type="button" title="Edit criteria">
                      <Icon.Edit />
                    </button>
                  </div>
                  <div className="tmpl-tags">
                    {t.tags.map(tag => <span key={tag} className="tmpl-tag">{tag}</span>)}
                  </div>

                  <div className="tmpl-actions-row">
                    {isRunning ? (
                      <div className="tmpl-running">
                        <span className="spin"></span>
                        Running…
                      </div>
                    ) : t.active ? (
                      <span className="tmpl-currently">Currently viewing this analysis</span>
                    ) : t.result ? (
                      <>
                        <button className="btn btn-sm" type="button" onClick={() => onSetActive(t.id)}>
                          View analysis →
                        </button>
                        <button className="btn btn-sm btn-ghost" type="button" onClick={() => onSetActive(t.id)}>
                          Set as default
                        </button>
                      </>
                    ) : (
                      <button className="btn btn-sm" type="button" onClick={() => handleRun(t)}>
                        <Icon.Sparkle /> Run analysis
                      </button>
                    )}
                  </div>
                </div>
              );
            })}
          </div>
        </div>
        <div className="drawer-foot">
          <button className="btn btn-ghost" onClick={onClose} type="button">Close</button>
          <button className="btn" type="button"><Icon.Plus /> Upload firm template</button>
          <button className="btn btn-primary" type="button"><Icon.Sparkle /> New template</button>
        </div>
      </div>
    </>
  );
}

function PlaybookDrawer({
  data,
  onClose,
  prefocusCriterionId,
  prefocusTemplateId,
  onSaveCriterion,
  onAddCriterion,
  onDeleteCriterion
}) {
  const activeTemplate = (data.templates || []).find(t => t.id === (prefocusTemplateId || data.playbook.id)) || data.playbook;
  const criteriaSource = data.playbook.criteria || [];
  const [drafts, setDrafts] = useState({});
  const [adding, setAdding] = useState(false);
  const [newName, setNewName] = useState("");
  const [newRequirements, setNewRequirements] = useState("");
  const [busyId, setBusyId] = useState(null);

  useEffect(() => {
    const next = {};
    criteriaSource.forEach(c => {
      next[c.id] = c.preferred || c.requirements || "";
    });
    setDrafts(next);
  }, [data.playbook.id, criteriaSource.length]);

  // Header shows the focused template
  const headerName = activeTemplate.name;
  const headerCount = activeTemplate.criteriaCount || data.playbook.criteriaCount;
  const headerEdited = activeTemplate.lastEdited ? `Edited ${activeTemplate.lastEdited}${activeTemplate.author ? " by " + activeTemplate.author : ""}` : data.playbook.lastEdited;
  const grouped = useMemo(() => {
    if (criteriaSource.length > 0) {
      const byArea = {};
      criteriaSource.forEach(criterion => {
        const area = criterion.area || criterion.title || "General";
        if (!byArea[area]) byArea[area] = [];
        byArea[area].push(criterion);
      });
      return Object.entries(byArea).map(([area, criteria]) => ({ area, criteria }));
    }

    // Mock: invent 4 criteria from clause areas for display
    const cas = data.playbook.clauseAreas;
    return cas.map((area, i) => ({
      area,
      criteria: [
        // For "Liability" — surface the liability cap criterion as the prefocused one
        ...(area === "Liability" ? [{
          id: "crit-liability-cap",
          title: "Liability cap",
          severity: "high",
          preferred: "Cap = total fees paid under the Agreement, with carve-outs for confidentiality, IP, data, indemnity, gross negligence/willful misconduct.",
          fallback: "12-month look-back with confidentiality + IP + data carve-outs.",
          unacceptable: "Less than 12 months fees; no carve-outs.",
          edited: "Edited yesterday"
        }] : []),
        ...(area === "Term & Renewal" ? [{
          id: "crit-term-conv",
          title: "Customer termination for convenience",
          severity: "high",
          preferred: "30 days notice, refund of prepaid unused fees.",
          fallback: "60 days notice acceptable for multi-year subscriptions.",
          unacceptable: "No customer termination right.",
          edited: "Edited 3 days ago"
        }, {
          id: "crit-auto-renew",
          title: "Auto-renewal opt-in",
          severity: "medium",
          preferred: "Opt-in renewal preferred; if not, 60-day opt-out window + 90-day supplier notice.",
          fallback: "30-day opt-out with 60-day supplier notice.",
          unacceptable: "Auto-renewal without notice from supplier."
        }] : []),
        ...(area === "Confidentiality" ? [{
          id: "crit-conf",
          title: "Confidentiality survival",
          severity: "medium",
          preferred: "5 years post-termination; indefinite for trade secrets."
        }] : []),
        ...(area === "Indemnity" ? [{
          id: "crit-indem",
          title: "Indemnification scope",
          severity: "high",
          preferred: "Supplier indemnity for IP infringement, data breach, confidentiality breach, gross negligence."
        }] : [])
      ]
    }));
  }, [data.playbook, criteriaSource]);

  const saveCriterion = async (criterion) => {
    setBusyId(criterion.id);
    try {
      await onSaveCriterion(criterion.id, drafts[criterion.id] || "");
    } finally {
      setBusyId(null);
    }
  };

  const deleteCriterion = async (criterion) => {
    setBusyId(criterion.id);
    try {
      await onDeleteCriterion(criterion.id);
    } finally {
      setBusyId(null);
    }
  };

  const addCriterion = async () => {
    if (!newName.trim() || !newRequirements.trim()) return;
    setBusyId("new");
    try {
      await onAddCriterion({
        name: newName.trim(),
        requirements: newRequirements.trim(),
      });
      setNewName("");
      setNewRequirements("");
      setAdding(false);
    } finally {
      setBusyId(null);
    }
  };

  return (
    <>
      <div className="scrim" onClick={onClose}></div>
      <div className="drawer wide" role="dialog" aria-label="Playbook editor">
        <div className="drawer-head">
          <h2>{headerName}</h2>
          <span className="ext-meta">{headerCount} criteria · {headerEdited}</span>
          <button className="close" onClick={onClose} aria-label="Close"><Icon.Close /></button>
        </div>
        <div className="drawer-body">
          <div style={{
            padding: "10px 14px",
            background: "var(--med-bg)",
            border: "1px solid var(--med-edge)",
            borderRadius: 6,
            marginBottom: 16,
            fontSize: 12.5,
            color: "var(--ink-2)",
            display: "flex", gap: 10, alignItems: "flex-start"
          }}>
            <div style={{
              width: 18, height: 18, borderRadius: "50%",
              background: "var(--med)", color: "var(--paper)",
              display: "grid", placeItems: "center", flexShrink: 0,
              fontFamily: "var(--mono)", fontSize: 11, fontWeight: 600
            }}>i</div>
            <div>
              Editing here updates the playbook for <b>future analyses</b>. Past analyses (including this one) keep the playbook version used at the time of review.
            </div>
          </div>

          <div className="history-search" style={{marginBottom: 16}}>
            <span className="glass"><Icon.Search /></span>
            <input placeholder="Search criteria…" />
          </div>

          {adding && (
            <div style={{
              padding: "12px 14px",
              background: "var(--paper-doc)",
              border: "1px solid var(--border-soft)",
              borderRadius: 6,
              marginBottom: 16
            }}>
              <div className="form-group">
                <div className="form-label">Criterion name</div>
                <input
                  id="newCriterionName"
                  className="input"
                  value={newName}
                  onChange={(event) => setNewName(event.target.value)}
                  placeholder="Payment terms"
                />
              </div>
              <div className="form-group">
                <div className="form-label">Standard position</div>
                <AutoTextarea
                  id="newCriterionRequirements"
                  className="input"
                  rows={4}
                  value={newRequirements}
                  onChange={(event) => setNewRequirements(event.target.value)}
                  placeholder="Preferred position, fallback, unacceptable language..."
                />
              </div>
              <div style={{display: "flex", gap: 6, justifyContent: "flex-end"}}>
                <button className="btn btn-sm btn-ghost" type="button" onClick={() => setAdding(false)}>
                  Cancel
                </button>
                <button
                  id="createCriterion"
                  className="btn btn-sm btn-primary"
                  type="button"
                  disabled={busyId === "new" || !newName.trim() || !newRequirements.trim()}
                  onClick={addCriterion}
                >
                  Add criterion
                </button>
              </div>
            </div>
          )}

          {grouped.map(g => (
            <div key={g.area} style={{marginBottom: 18}}>
              <div style={{
                display: "flex", alignItems: "baseline", gap: 8,
                marginBottom: 8, paddingBottom: 6,
                borderBottom: "1px solid var(--border-soft)"
              }}>
                <div style={{fontFamily: "var(--mono)", fontSize: 10.5, letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--ink-muted)"}}>
                  Clause area
                </div>
                <div style={{fontSize: 13.5, fontWeight: 600, color: "var(--ink)"}}>{g.area}</div>
                <div style={{marginLeft: "auto", fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-muted)"}}>
                  {g.criteria.length || 0} criteria
                </div>
              </div>
              {g.criteria.length === 0 ? (
                <div style={{padding: "6px 4px", color: "var(--ink-muted)", fontSize: 12.5}}>
                  Other criteria in this area are collapsed for clarity.
                </div>
              ) : g.criteria.map(c => (
                <div key={c.id} style={{
                  padding: "12px 14px",
                  background: prefocusCriterionId === c.id ? "var(--accent-soft)" : "var(--paper-doc)",
                  border: "1px solid var(--border-soft)",
                  borderRadius: 6,
                  marginBottom: 8
                }}>
                  <div style={{display: "flex", alignItems: "center", gap: 8, marginBottom: 6}}>
                    <SevPill severity={c.severity} />
                    <div style={{fontWeight: 600}}>{c.title}</div>
                    {c.edited && <div style={{marginLeft: "auto", fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--ink-muted)"}}>{c.edited}</div>}
                  </div>
                  {c.preferred && (
                    <div style={{marginBottom: 6}}>
                      <div className="form-label" style={{margin: 0, marginBottom: 2}}>Preferred</div>
                      {criteriaSource.length > 0 ? (
                        <AutoTextarea
                          className="input"
                          rows={3}
                          value={drafts[c.id] ?? c.preferred}
                          onChange={(event) => setDrafts(current => ({ ...current, [c.id]: event.target.value }))}
                        />
                      ) : (
                        <div style={{fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5}}>{c.preferred}</div>
                      )}
                    </div>
                  )}
                  {c.fallback && (
                    <div style={{marginBottom: 6}}>
                      <div className="form-label" style={{margin: 0, marginBottom: 2}}>Fallback</div>
                      <div style={{fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5}}>{c.fallback}</div>
                    </div>
                  )}
                  {c.unacceptable && (
                    <div style={{marginBottom: 8}}>
                      <div className="form-label" style={{margin: 0, marginBottom: 2, color: "var(--high)"}}>Unacceptable</div>
                      <div style={{fontSize: 12.5, color: "var(--ink-2)", lineHeight: 1.5}}>{c.unacceptable}</div>
                    </div>
                  )}
                  <div style={{display: "flex", gap: 6}}>
                    <button
                      className="btn btn-sm"
                      type="button"
                      disabled={busyId === c.id}
                      onClick={() => saveCriterion(c)}
                    >
                      <Icon.Edit /> Save
                    </button>
                    <button
                      className="btn btn-sm btn-ghost"
                      type="button"
                      onClick={() => {
                        setNewName(c.title || "");
                        setNewRequirements(drafts[c.id] || c.preferred || "");
                        setAdding(true);
                      }}
                    >
                      Duplicate
                    </button>
                    <button
                      className="btn btn-sm btn-ghost"
                      type="button"
                      disabled={busyId === c.id}
                      onClick={() => deleteCriterion(c)}
                      style={{marginLeft: "auto", color: "var(--high)"}}
                    >
                      Delete
                    </button>
                  </div>
                </div>
              ))}
            </div>
          ))}
        </div>
        <div className="drawer-foot">
          <button className="btn btn-ghost" onClick={onClose} type="button">Close</button>
          <button id="addCriterion" className="btn" type="button" onClick={() => setAdding(true)}><Icon.Plus /> Add criterion</button>
          <button className="btn btn-primary" type="button">Save changes</button>
        </div>
      </div>
    </>
  );
}

// ============================================================================
// Live API adapter: map Playbook backend shape into the PapersVal UI model.
// This keeps the imported prototype reusable while the backend can stay stable.
// ============================================================================
function getApiKey() {
  return localStorage.getItem("playbook_api_key") || "";
}

async function api(path, options = {}) {
  const headers = new Headers(options.headers || {});
  const apiKey = getApiKey();
  if (apiKey) headers.set("X-API-Key", apiKey);
  const response = await fetch(path, { ...options, headers });
  if (!response.ok) {
    throw new Error((await response.text()) || `Request failed: ${response.status}`);
  }
  if (response.status === 204) return null;
  const text = await response.text();
  return text ? JSON.parse(text) : null;
}

function queryParam(name) {
  return new URLSearchParams(window.location.search).get(name);
}

function appLink(analysisId, findingId = null) {
  const url = new URL(window.location.href);
  url.searchParams.set("analysis", String(analysisId));
  if (findingId) url.searchParams.set("check", String(findingId));
  else url.searchParams.delete("check");
  return `${url.pathname}${url.search}`;
}

function setRoute(analysisId, findingId = null) {
  window.history.replaceState({}, "", appLink(analysisId, findingId));
}

function normalizeStatus(status) {
  return {
    meets: "match",
    partially_meets: "partial",
    not_meets: "does_not_match",
    missing: "not_found",
    not_applicable: "n_a",
    error: "needs_review",
  }[status] || status || "needs_review";
}

function normalizeSeverity(severity, status) {
  if (status === "meets" || status === "not_applicable") return "low";
  if (severity === "none") return "low";
  if (["high", "medium", "low", "info"].includes(severity)) return severity;
  return "medium";
}

function areaFromCheck(check) {
  if (check.criteria_name && !String(check.criteria_name).startsWith("Criterion ")) {
    return check.criteria_name;
  }
  if (check.essence_tag) {
    return String(check.essence_tag)
      .split("_")
      .map(part => part.charAt(0).toUpperCase() + part.slice(1))
      .join(" ");
  }
  return `Criterion ${check.criteria_id}`;
}

function fieldFromRequirements(requirements, label) {
  if (!requirements) return null;
  const found = String(requirements).match(new RegExp(`^${label}:\\s*(.+)$`, "m"));
  return found ? found[1].trim() : null;
}

function buildSections(lines) {
  const safeLines = lines && lines.length ? lines : [
    { line_no: 1, text: "No parsed contract text is available yet." }
  ];
  const sections = [];
  let current = null;

  const startSection = (line, fallbackIndex) => {
    const heading = String(line.text || "").replace(/^#+\s*/, "").trim();
    const numberMatch = heading.match(/^(\d+)[.)]?\s*(.*)$/);
    const number = numberMatch ? numberMatch[1] : String(fallbackIndex);
    const title = numberMatch ? (numberMatch[2] || heading) : heading;
    current = {
      id: `s-${line.line_no}`,
      number,
      title: title || `Section ${number}`,
      lineStart: line.line_no,
      lineEnd: line.line_no,
      paragraphs: [],
      paragraphLines: [],
      marks: [],
    };
    sections.push(current);
  };

  safeLines.forEach((line) => {
    const text = String(line.text || "").trim();
    if (!text) return;
    const isHeading = text.startsWith("#") || /^(\d+)[.)]\s+\S/.test(text);
    if (!current || isHeading) {
      startSection(line, sections.length + 1);
      if (text.startsWith("#")) return;
    }
    current.paragraphs.push(text);
    current.paragraphLines.push(line.line_no);
    current.lineEnd = line.line_no;
  });

  return sections.length ? sections : [{
    id: "s-1",
    number: "1",
    title: "Contract",
    lineStart: 1,
    lineEnd: safeLines[safeLines.length - 1].line_no || 1,
    paragraphs: safeLines.map(line => line.text),
    paragraphLines: safeLines.map(line => line.line_no),
    marks: [],
  }];
}

function sectionForLine(sections, lineNo) {
  return sections.find(section => lineNo >= section.lineStart && lineNo <= section.lineEnd) || sections[0];
}

function addFindingMark(section, findingId, evidence) {
  if (!section || !evidence?.line_start) return;
  const paragraphIndex = Math.max(
    0,
    section.paragraphLines.findIndex(lineNo => lineNo >= evidence.line_start)
  );
  const text = section.paragraphs[paragraphIndex] || "";
  if (!text) return;
  section.marks.push({
    findingId,
    paragraph: paragraphIndex,
    start: 0,
    end: text.length,
  });
}

function countsFromFindings(findings) {
  const counts = {
    total: findings.length,
    high: 0,
    medium: 0,
    low: 0,
    info: 0,
    extra: 0,
    match: 0,
    partial: 0,
    does_not_match: 0,
    not_found: 0,
    needs_review: 0,
    n_a: 0,
  };
  findings.forEach(finding => {
    counts[finding.severity] = (counts[finding.severity] || 0) + 1;
    counts[finding.status] = (counts[finding.status] || 0) + 1;
    if (finding.status === "extra") counts.extra += 1;
  });
  return counts;
}

function normalizeCriterion(item, index = 0) {
  const id = String(item.contract_type_criteria_id || item.id || item.criteria_id || `criterion-${index + 1}`);
  const title = item.name || item.title || `Criterion ${item.criteria_id || index + 1}`;
  const requirements = item.requirements || item.preferred || "";
  return {
    id,
    contractTypeCriteriaId: item.contract_type_criteria_id || item.contractTypeCriteriaId || item.id || null,
    criteriaId: item.criteria_id || item.criteriaId || null,
    title,
    name: title,
    area: title,
    essenceTag: item.essence_tag || item.essenceTag || null,
    priority: item.priority || index + 1,
    severity: item.criticality || item.severity || "medium",
    preferred: requirements,
    requirements,
  };
}

function refreshPlaybookShape(playbook, criteria) {
  const normalizedCriteria = (criteria || []).map(normalizeCriterion);
  const clauseAreas = [...new Set(normalizedCriteria.map(item => item.area).filter(Boolean))].slice(0, 12);
  return {
    ...playbook,
    criteria: normalizedCriteria,
    criteriaCount: normalizedCriteria.length || playbook.criteriaCount || 40,
    clauseAreas: clauseAreas.length ? clauseAreas : playbook.clauseAreas,
  };
}

function transformAnalysisDetail(detail, context = {}) {
  const analysis = detail.analysis || {};
  const sections = buildSections(detail.lines || []);
  const findings = (detail.checks || []).map((check, index) => {
    const evidence = (check.evidence || [])[0] || null;
    const section = evidence ? sectionForLine(sections, evidence.line_start) : null;
    const status = normalizeStatus(check.status);
    const severity = normalizeSeverity(check.severity, check.status);
    const area = areaFromCheck(check);
    const id = String(check.id || `${analysis.id || "analysis"}-${check.criteria_id || index + 1}`);
    const finding = {
      id,
      title: area,
      area,
      severity,
      status,
      sectionRef: section ? `Section ${section.number}` : "—",
      sectionId: section ? section.id : null,
      summary: check.reason || "No reason recorded.",
      firmPosition:
        fieldFromRequirements(check.requirements, "Preferred position") ||
        fieldFromRequirements(check.requirements, "Preferred wording") ||
        check.requirements ||
        "No playbook requirement text is available.",
      foundQuote: evidence?.text || null,
      whyItMatters: check.reason || "Review against the active playbook position.",
      suggested: check.suggested_redline || "",
      candidates: evidence ? null : [area],
    };
    if (section && status !== "match" && status !== "n_a") {
      addFindingMark(section, id, evidence);
    }
    return finding;
  });

  const file = analysis.filename || "Contract analysis";
  return {
    analysisId: analysis.id || context.analysisId || "demo",
    contract: {
      filename: file,
      party: file.replace(/\.[^.]+$/, ""),
      counterparty: analysis.contract_type || "Active playbook",
      effectiveDate: analysis.uploaded_at
        ? new Date(analysis.uploaded_at).toLocaleDateString()
        : "Demo",
      sections,
    },
    findings,
    counts: countsFromFindings(findings),
    playbook: context.playbook || {
      id: String(analysis.contract_type_id || "active-playbook"),
      contractTypeId: analysis.contract_type_id || null,
      playbookId: null,
      name: analysis.contract_type || "Demo: Synthetic MSA Standard",
      version: "v1.0",
      versionId: "current",
      criteriaCount: findings.length,
      criteria: [],
      clauseAreas: [...new Set(findings.map(f => f.area))].slice(0, 8),
      lastEdited: "Loaded from current workspace",
    },
    templates: context.templates || [],
    history: context.history || [],
  };
}

function localDetailFromDemoDocument(documentInfo, criteria = []) {
  const criteriaById = {};
  (criteria || []).forEach((criterion, index) => {
    const normalized = normalizeCriterion(criterion, index);
    criteriaById[String(criterion.contract_type_criteria_id)] = normalized;
    criteriaById[String(criterion.criteria_id)] = normalized;
    criteriaById[String(normalized.id)] = normalized;
  });
  const lines = String(documentInfo.text || "")
    .split(/\r?\n/)
    .map(line => line.trim())
    .filter(Boolean)
    .map((text, index) => ({ id: `local-${index + 1}`, line_no: index + 1, page_no: 1, text }));
  return {
    analysis: {
      id: documentInfo.document_id,
      filename: documentInfo.filename,
      uploaded_at: null,
      contract_type_id: "demo-local",
      contract_type: "Demo: Synthetic MSA Standard",
      batch_name: "Demo Synthetic MSA Fixtures",
      status: "parsed",
      result_count: documentInfo.results?.length || 0,
      status_counts: {},
      severity_counts: {},
    },
    lines,
    checks: (documentInfo.results || []).map((result, index) => ({
      id: `${documentInfo.document_id}-${result.criteria_id || index + 1}`,
      criteria_id: result.criteria_id,
      contract_type_criteria_id: result.contract_type_criteria_id || result.criteria_id,
      criteria_name: criteriaById[String(result.contract_type_criteria_id || result.criteria_id)]?.title || `Criterion ${result.criteria_id}`,
      essence_tag: criteriaById[String(result.contract_type_criteria_id || result.criteria_id)]?.essenceTag || null,
      requirements: criteriaById[String(result.contract_type_criteria_id || result.criteria_id)]?.requirements || null,
      status: result.status,
      reason: result.reason,
      deviation_type: result.deviation_type,
      severity: result.severity,
      suggested_redline: result.suggested_redline,
      evidence: [],
      source_state: result.status === "missing" ? "not_found" : "not_applicable",
    })),
  };
}

function transformHistory(analyses) {
  return (analyses || []).map(item => {
    const statusCounts = item.status_counts || {};
    const severityCounts = item.severity_counts || {};
    return {
      id: String(item.id),
      name: item.filename || `Analysis ${item.id}`,
      matter: item.batch_name || item.contract_type || "Unfiled",
      status: item.result_count ? "ready" : item.status === "failed" ? "failed" : "comparing",
      high: severityCounts.high || 0,
      medium: severityCounts.medium || 0,
      missing: statusCounts.missing || 0,
      when: item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : "Demo",
      progress: item.result_count || 0,
      total: 40,
    };
  });
}

function playbookFromDemo(demo, criteria) {
  const normalizedCriteria = (criteria || []).map(normalizeCriterion);
  const clauseAreas = [...new Set(normalizedCriteria.map(item => item.area).filter(Boolean))].slice(0, 12);
  return {
    id: String(demo.contract_type_id || demo.playbook_id || "demo-playbook"),
    contractTypeId: demo.contract_type_id || null,
    playbookId: demo.playbook_id || null,
    name: "Demo: Synthetic MSA Standard",
    version: "v1.0",
    versionId: String(demo.playbook_id || "demo-playbook"),
    criteriaCount: normalizedCriteria.length || 40,
    criteria: normalizedCriteria,
    clauseAreas,
    lastEdited: "Seeded demo standard",
  };
}

function templatesFor(playbook) {
  return [
    {
      ...playbook,
      active: true,
      author: "Demo",
      lastEdited: playbook.lastEdited || "Current",
      tags: ["MSA", "Vendor paper", "Demo"],
      result: null,
    },
    {
      id: "future-saas-vendor",
      name: "SaaS Vendor MSA",
      docType: "Master Services Agreement",
      version: "vNext",
      criteriaCount: 40,
      author: "Template library",
      lastEdited: "Sample",
      active: false,
      tags: ["SaaS", "Vendor paper"],
      result: null,
    },
  ];
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function loadWorkspace(analysisId = queryParam("analysis"), options = {}) {
  const allowFallback = options.allowFallback !== false;
  try {
    const demo = await api("/api/demo/seed", { method: "POST" });
    const criteria = demo.criteria || [];
    const playbook = playbookFromDemo(demo, criteria);
    const templates = templatesFor(playbook);

    if (demo.local_demo) {
      const docs = demo.incoming_documents || [];
      const history = transformHistory(docs.map(doc => ({
        id: doc.document_id,
        filename: doc.filename,
        batch_name: "Demo Synthetic MSA Fixtures",
        result_count: doc.report_result_count,
        status_counts: {},
        severity_counts: {},
      })));
      const selectedDoc =
        docs.find(doc => String(doc.document_id) === String(analysisId)) ||
        docs[0];
      const detail = localDetailFromDemoDocument(selectedDoc, criteria);
      return transformAnalysisDetail(detail, { playbook, templates, history });
    }

    const analysesPayload = await api("/api/analyses/");
    const history = transformHistory(analysesPayload.analyses || []);
    const selected =
      (analysisId && history.find(item => String(item.id) === String(analysisId))) ||
      history[0];
    const selectedId = selected?.id || demo.incoming_documents?.[0]?.document_id;
    const detail = await api(`/api/analyses/${selectedId}`);
    return transformAnalysisDetail(detail, { playbook, templates, history });
  } catch (error) {
    if (!allowFallback) throw error;
    console.warn("Falling back to bundled PapersVal demo data", error);
    return window.DATA;
  }
}

async function waitForAnalysisReady(analysisId, options = {}) {
  const timeoutMs = options.timeoutMs || 5 * 60 * 1000;
  const intervalMs = options.intervalMs || 3000;
  const startedAt = Date.now();
  let latest = null;

  while (Date.now() - startedAt < timeoutMs) {
    latest = await loadWorkspace(analysisId, { allowFallback: false });
    const historyItem = (latest.history || []).find(item => String(item.id) === String(analysisId));
    if ((latest.findings || []).length > 0 || historyItem?.status === "ready" || historyItem?.status === "failed") {
      return latest;
    }
    await sleep(intervalMs);
  }

  return latest;
}

// ============================================================================
// App
// ============================================================================
function App() {
  const [data, setData] = useState(window.DATA);
  const [loadingLiveData, setLoadingLiveData] = useState(true);
  const findingsById = useMemo(() => {
    const m = {};
    data.findings.forEach(f => { m[f.id] = f; });
    return m;
  }, [data.findings]);

  // Default-select the highest-priority finding so the demo lands on the moment
  const [selectedFindingId, setSelectedFindingId] = useState("f-liability");
  const [activeAnalysisId, setActiveAnalysisId] = useState(String(data.analysisId || "a-103"));
  const [filters, setFilters] = useState({
    cats: new Set(),
    resolution: "unresolved",
    showMatches: false
  });
  // Per-finding resolution state. Stored as { [findingId]: { resolution: 'accepted'|'replaced'|'noted', note?, replaced? } }
  const [findingsState, setFindingsState] = useState({});
  const [drawerUpload, setDrawerUpload] = useState(false);
  const [drawerTemplates, setDrawerTemplates] = useState(false);
  const [drawerPlaybook, setDrawerPlaybook] = useState(null); // null | { templateId }
  const [drawerExport, setDrawerExport] = useState(false);
  const [activeTemplateId, setActiveTemplateId] = useState(data.playbook.id);
  const [sidebarOpen, setSidebarOpen] = useState(true);
  const [reanalyzing, setReanalyzing] = useState(false);
  const [viewMode, setViewMode] = useState("review"); // 'review' | 'final'
  const [toast, setToast] = useState(null);

  const activeTemplate = useMemo(
    () => data.templates.find(t => t.id === activeTemplateId) || data.templates[0],
    [data.templates, activeTemplateId]
  );

  const viewerRef = useRef(null);

  useEffect(() => {
    let cancelled = false;
    loadWorkspace().then(nextData => {
      if (cancelled) return;
      const firstFinding = nextData.findings.find(f => f.status !== "match" && f.status !== "n_a") || nextData.findings[0];
      const requestedCheck = queryParam("check");
      setData(nextData);
      setActiveAnalysisId(String(nextData.analysisId || nextData.history?.[0]?.id || "demo"));
      setActiveTemplateId(nextData.playbook.id);
      setSelectedFindingId(requestedCheck || firstFinding?.id || null);
      setLoadingLiveData(false);
      if (nextData.analysisId) setRoute(nextData.analysisId, requestedCheck || firstFinding?.id || null);
    });
    return () => { cancelled = true; };
  }, []);

  const scrollToFinding = useCallback((id, smooth = true) => {
    const f = findingsById[id];
    if (!f) return;
    const targetId = f.sectionId ? `sec-${f.sectionId}` : `miss-${f.id}`;
    const el = document.getElementById(targetId);
    const scroller = viewerRef.current;
    if (el && scroller) {
      const offset = el.offsetTop - 72;
      scroller.scrollTo({ top: offset, behavior: smooth ? "smooth" : "auto" });
    }
  }, [findingsById]);

  const handleSelectFinding = useCallback((id) => {
    // Toggle: clicking the same finding closes it
    setSelectedFindingId(prev => {
      const next = prev === id ? null : id;
      setRoute(activeAnalysisId, next);
      return next;
    });
    if (!id) return;
    setTimeout(() => scrollToFinding(id, true), 30);
  }, [activeAnalysisId, scrollToFinding]);

  // Initial mount: scroll to the default-selected finding so the demo lands on it.
  useEffect(() => {
    if (!selectedFindingId) return;
    let raf;
    const tick = () => {
      const f = findingsById[selectedFindingId];
      const targetId = f.sectionId ? `sec-${f.sectionId}` : `miss-${f.id}`;
      const el = document.getElementById(targetId);
      const scroller = viewerRef.current;
      if (el && scroller && scroller.scrollHeight > scroller.clientHeight) {
        scroller.scrollTo({ top: el.offsetTop - 72, behavior: "auto" });
      } else {
        raf = requestAnimationFrame(tick);
      }
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []); // eslint-disable-line

  useEffect(() => {
    if (!selectedFindingId) return;
    const timer = setTimeout(() => scrollToFinding(selectedFindingId, false), 80);
    return () => clearTimeout(timer);
  }, [data.analysisId, selectedFindingId, scrollToFinding]);

  const handleJumpTo = useCallback((sectionId) => {
    const scroller = viewerRef.current;
    const el = document.getElementById(`sec-${sectionId}`);
    if (el && scroller) {
      const offset = el.offsetTop - 72;
      scroller.scrollTo({ top: offset, behavior: "smooth" });
    }
  }, []);

  const showToast = useCallback((msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 1800);
  }, []);

  const applyAnalysisData = useCallback((nextData, requestedId = null) => {
    const firstFinding = nextData.findings.find(f => f.status !== "match" && f.status !== "n_a") || nextData.findings[0];
    setData(nextData);
    setActiveAnalysisId(String(nextData.analysisId || requestedId));
    setSelectedFindingId(firstFinding?.id || null);
    setRoute(nextData.analysisId || requestedId, firstFinding?.id || null);
  }, []);

  const handleSelectAnalysis = useCallback(async (id) => {
    showToast("Loading analysis…");
    const nextData = await loadWorkspace(id);
    applyAnalysisData(nextData, id);
  }, [applyAnalysisData, showToast]);

  const setFiltersAndSync = useCallback((newFilters) => {
    setFilters(newFilters);
    // If a finding is selected and the new filter excludes it, close detail
    if (!selectedFindingId) return;
    const f = findingsById[selectedFindingId];
    if (!f) return;
    const active = newFilters.cats;
    const passesCat = newFilters.showMatches ? (f.status === "match") :
      (f.status === "match" || f.status === "n_a") ? false :
      active.size === 0 ? true :
      (active.has("high") && f.severity === "high") ||
      (active.has("medium") && f.severity === "medium") ||
      (active.has("low") && f.severity === "low") ||
      (active.has("not_found") && f.status === "not_found") ||
      (active.has("extra") && f.status === "extra");
    if (!passesCat) {
      setSelectedFindingId(null);
    }
  }, [selectedFindingId, findingsById]);

  const handleReanalyze = useCallback(() => {
    setReanalyzing(true);
    showToast(`Re-running analysis against ${activeTemplate.name}…`);
    setTimeout(() => {
      setReanalyzing(false);
      showToast("Analysis refreshed");
    }, 1500);
  }, [activeTemplate, showToast]);

  // Filter predicate — used both to filter the right panel list AND dim/highlight in the contract
  const filterPredicate = useMemo(() => {
    const active = filters.cats;
    const resolutionFilter = filters.resolution;
    const showMatches = filters.showMatches;
    return (f) => {
      if (showMatches) return f.status === "match";
      if (f.status === "match" || f.status === "n_a") return false;
      if (active.size > 0) {
        let catPass = false;
        if (active.has("high") && f.severity === "high") catPass = true;
        if (active.has("medium") && f.severity === "medium") catPass = true;
        if (active.has("low") && f.severity === "low") catPass = true;
        if (active.has("not_found") && f.status === "not_found") catPass = true;
        if (active.has("extra") && f.status === "extra") catPass = true;
        if (!catPass) return false;
      }
      const res = findingsState[f.id]?.resolution;
      if (resolutionFilter === "unresolved" && res) return false;
      if (resolutionFilter === "resolved" && !res) return false;
      return true;
    };
  }, [filters, findingsState]);

  const filterActive = filters.cats.size > 0 || filters.resolution !== "unresolved" || filters.showMatches;

  const handleCopyLink = () => {
    const url = `${window.location.origin}${appLink(activeAnalysisId, selectedFindingId)}`;
    navigator.clipboard?.writeText(url).catch(() => {});
    showToast(`Link copied — ${url}`);
  };

  const handleExport = () => {
    setDrawerExport(true);
  };

  const activeContractTypeId = () => {
    const value = data.playbook.contractTypeId || data.playbook.id;
    return /^\d+$/.test(String(value)) ? Number(value) : null;
  };

  const activePlaybookId = () => {
    const value = data.playbook.playbookId || data.playbook.versionId;
    return /^\d+$/.test(String(value)) ? Number(value) : null;
  };

  const setPlaybookCriteria = useCallback((criteria) => {
    setData(current => {
      const playbook = refreshPlaybookShape(current.playbook, criteria);
      const templates = current.templates.map(template =>
        template.id === current.playbook.id
          ? { ...template, criteriaCount: playbook.criteriaCount, clauseAreas: playbook.clauseAreas }
          : template
      );
      return { ...current, playbook, templates };
    });
  }, []);

  const handleSaveCriterion = useCallback(async (criterionId, requirements) => {
    const playbookId = activePlaybookId();
    if (playbookId && /^\d+$/.test(String(criterionId))) {
      await api(`/api/playbooks/${playbookId}/criteria/${criterionId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ requirements }),
      });
    }
    const nextCriteria = (data.playbook.criteria || []).map(criterion =>
      String(criterion.id) === String(criterionId)
        ? { ...criterion, preferred: requirements, requirements }
        : criterion
    );
    setPlaybookCriteria(nextCriteria);
    showToast("Criterion saved");
  }, [data.playbook, setPlaybookCriteria, showToast]);

  const handleAddCriterion = useCallback(async ({ name, requirements }) => {
    const playbookId = activePlaybookId();
    let created = {
      id: `local-${Date.now()}`,
      name,
      title: name,
      requirements,
      preferred: requirements,
      priority: (data.playbook.criteria || []).length + 1,
    };
    if (playbookId) {
      created = await api(`/api/playbooks/${playbookId}/criteria`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, requirements, criticality: "medium" }),
      });
    }
    setPlaybookCriteria([...(data.playbook.criteria || []), created]);
    showToast("Criterion added");
  }, [data.playbook, setPlaybookCriteria, showToast]);

  const handleDeleteCriterion = useCallback(async (criterionId) => {
    const playbookId = activePlaybookId();
    if (playbookId && /^\d+$/.test(String(criterionId))) {
      await api(`/api/playbooks/${playbookId}/criteria/${criterionId}`, { method: "DELETE" });
    }
    const nextCriteria = (data.playbook.criteria || []).filter(criterion => String(criterion.id) !== String(criterionId));
    setPlaybookCriteria(nextCriteria);
    showToast("Criterion deleted");
  }, [data.playbook, setPlaybookCriteria, showToast]);

  const openAnalysisAfterCreate = async (documentId, options = {}) => {
    const shouldQueueCompare = options.queueCompare !== false;
    if (shouldQueueCompare) {
      try {
        await api(`/api/documents/${documentId}/compare`, { method: "POST" });
        showToast("Comparison queued");
      } catch (error) {
        showToast("Uploaded; comparison is not queued yet");
      }
    } else {
      showToast("File uploaded; processing queued");
    }

    await handleSelectAnalysis(documentId);

    try {
      const readyData = await waitForAnalysisReady(documentId, {
        timeoutMs: options.timeoutMs || 6 * 60 * 1000,
      });
      if (readyData) {
        applyAnalysisData(readyData, documentId);
        if ((readyData.findings || []).length > 0) showToast("Analysis ready");
      }
    } catch (error) {
      console.warn("Timed out while waiting for analysis readiness", error);
    }
  };

  const handleAnalyzeFile = async (file) => {
    const form = new FormData();
    form.append("files", file);
    form.append("batch_name", `Incoming - ${file.name}`);
    const contractTypeId = activeContractTypeId();
    if (contractTypeId) form.append("contract_type_id", String(contractTypeId));
    const result = await api("/api/documents/upload/", { method: "POST", body: form });
    const documentId = result.document_ids?.[0];
    if (documentId) await openAnalysisAfterCreate(documentId, { queueCompare: false });
  };

  const handleAnalyzeText = async (text) => {
    const contractTypeId = activeContractTypeId();
    const result = await api("/api/documents/paste", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        text,
        filename: "pasted-contract.txt",
        batch_name: "Incoming - pasted text",
        contract_type_id: contractTypeId,
      }),
    });
    if (result.document_id) await openAnalysisAfterCreate(result.document_id);
  };

  const handleImportGoogleDoc = async (url) => {
    const contractTypeId = activeContractTypeId();
    const result = await api("/api/documents/import-url", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        url,
        filename: "google-doc-contract.txt",
        batch_name: "Incoming - Google Doc",
        contract_type_id: contractTypeId,
      }),
    });
    if (result.document_id) await openAnalysisAfterCreate(result.document_id);
  };

  const handleSaveApiKey = (value) => {
    const trimmed = value.trim();
    if (trimmed) localStorage.setItem("playbook_api_key", trimmed);
    else localStorage.removeItem("playbook_api_key");
  };

  // Keyboard navigation: J/K to move through findings, Esc to close drawers
  useEffect(() => {
    const onKey = (e) => {
      if (drawerUpload || drawerTemplates || drawerPlaybook || drawerExport) {
        if (e.key === "Escape") {
          setDrawerUpload(false);
          setDrawerTemplates(false);
          setDrawerPlaybook(null);
          setDrawerExport(false);
        }
        return;
      }
      if (e.target && (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")) return;
      const order = data.findings.filter(f => f.status !== "match" && f.status !== "n_a").map(f => f.id);
      const idx = order.indexOf(selectedFindingId);
      if (e.key === "j" || e.key === "ArrowDown") {
        e.preventDefault();
        const next = order[Math.min(idx + 1, order.length - 1)];
        if (next) handleSelectFinding(next);
      }
      if (e.key === "k" || e.key === "ArrowUp") {
        e.preventDefault();
        const prev = order[Math.max(idx - 1, 0)];
        if (prev) handleSelectFinding(prev);
      }
      if (e.key === "Escape") setSelectedFindingId(null);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selectedFindingId, drawerUpload, drawerTemplates, drawerPlaybook, drawerExport, data.findings, handleSelectFinding]);

  const selectedFinding = selectedFindingId ? findingsById[selectedFindingId] : null;

  return (
    <div className={`app ${sidebarOpen ? "" : "no-sidebar"}`}>
      <TopBar data={data}
        sidebarOpen={sidebarOpen}
        onToggleSidebar={() => setSidebarOpen(s => !s)}
        activeTemplate={activeTemplate}
        onOpenUpload={() => setDrawerUpload(true)}
        onOpenTemplates={() => setDrawerTemplates(true)}
        onReanalyze={handleReanalyze}
        onCopyLink={handleCopyLink}
        onExport={handleExport}
      />
      <div className={`app-main ${sidebarOpen ? "" : "sidebar-hidden"}`}>
        {sidebarOpen && (
          <Sidebar data={data}
            activeAnalysisId={activeAnalysisId}
            onSelectAnalysis={handleSelectAnalysis}
            onNewAnalysis={() => setDrawerUpload(true)}
            onOpenPlaybook={() => setDrawerTemplates(true)}
          />
        )}
        <ContractViewer data={data}
          findingsById={findingsById}
          findingsState={findingsState}
          selectedFindingId={selectedFindingId}
          onSelectFinding={handleSelectFinding}
          filterMatch={filterActive ? filterPredicate : null}
          filterActive={filterActive}
          onApply={(id) => {
            setFindingsState(s => ({ ...s, [id]: { resolution: "replaced" } }));
            showToast(findingsById[id].status === "not_found" ? "Suggestion inserted" : "Suggestion applied");
          }}
          onAccept={(id) => {
            setFindingsState(s => ({ ...s, [id]: { resolution: "accepted" } }));
            showToast("Risk accepted");
          }}
          onAddNote={(id, note) => {
            setFindingsState(s => ({ ...s, [id]: { resolution: "noted", note } }));
            showToast("Comment saved");
          }}
          onUnresolve={(id) => {
            setFindingsState(s => { const n = {...s}; delete n[id]; return n; });
            showToast("Reopened");
          }}
          onOpenDetail={() => {}}
          viewMode={viewMode}
          onViewModeChange={setViewMode}
          viewerRef={viewerRef}
        />
        <div style={{position:"relative", minWidth: 0}}>
          <ReviewPanel data={data}
            filters={filters}
            setFilters={setFiltersAndSync}
            selectedFindingId={selectedFindingId}
            onSelectFinding={handleSelectFinding}
            findingsState={findingsState}
            selectedFinding={selectedFinding}
            onCopy={showToast}
            onJumpTo={handleJumpTo}
            onAccept={(reason) => {
              setFindingsState(s => ({ ...s, [selectedFinding.id]: { resolution: "accepted", acceptReason: reason } }));
              showToast("Risk accepted");
            }}
            onReplace={() => {
              setFindingsState(s => ({ ...s, [selectedFinding.id]: { resolution: "replaced" } }));
              showToast(selectedFinding.status === "not_found" ? "Clause inserted into contract" : "Suggestion applied");
            }}
            onAddNote={(note) => {
              setFindingsState(s => ({ ...s, [selectedFinding.id]: { resolution: "noted", note } }));
              showToast("Note added");
            }}
            onUnresolve={() => {
              setFindingsState(s => { const n = {...s}; delete n[selectedFinding.id]; return n; });
              showToast("Reopened");
            }}
            onCopyLink={handleCopyLink}
            onAddToExport={() => showToast("Added to export")}
            onMore={() => showToast("More options menu")}
          />
        </div>
      </div>
      {drawerUpload && (
        <UploadDrawer
          data={data}
          onClose={() => setDrawerUpload(false)}
          onAnalyzeFile={handleAnalyzeFile}
          onAnalyzeText={handleAnalyzeText}
          onImportGoogleDoc={handleImportGoogleDoc}
          onSaveApiKey={handleSaveApiKey}
        />
      )}
      {drawerExport && <ExportDrawer data={data} findingsState={findingsState} onClose={() => setDrawerExport(false)} onCopy={showToast} />}
      {drawerTemplates && (
        <TemplatesDrawer
          data={data}
          onClose={() => setDrawerTemplates(false)}
          onEditTemplate={(id) => { setDrawerTemplates(false); setDrawerPlaybook({ templateId: id }); }}
          onRunWithTemplate={(id) => { showToast(`Analysis complete — switched to template`); setActiveTemplateId(id); setDrawerTemplates(false); }}
          onSetActive={(id) => { showToast(`Switched active template`); setActiveTemplateId(id); setDrawerTemplates(false); }}
        />
      )}
      {drawerPlaybook && (
        <PlaybookDrawer
          data={data}
          onClose={() => setDrawerPlaybook(null)}
          onSaveCriterion={handleSaveCriterion}
          onAddCriterion={handleAddCriterion}
          onDeleteCriterion={handleDeleteCriterion}
          prefocusTemplateId={drawerPlaybook.templateId}
          prefocusCriterionId={
            selectedFinding?.id === "f-liability" ? "crit-liability-cap" :
            selectedFinding?.id === "f-termination" ? "crit-term-conv" :
            selectedFinding?.id === "f-auto-renew" ? "crit-auto-renew" : null
          }
        />
      )}
      {toast && <div className="toast">{toast}</div>}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
