'use client';

import React, { useEffect, useMemo, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import {
  FiSave, FiAlertTriangle, FiCheckCircle, FiZap, FiChevronLeft, FiChevronRight, FiSkipForward, FiCheck, FiList, FiImage,
} from 'react-icons/fi';
import { saveClausesBulk, raiseClauseNc, aiGenerateEvidence, aiGenerateFinding, listReportImages, updateReportHeader } from '@/lib/api/audit-report-builder.api';
import ImagesManager from './ImagesManager';
import RichPreview, { shortcutOf } from './RichPreview';
import { clearFormatting, toggleBold, toggleItalic, toggleUnderline } from './richText';
import { FINDING_OPTIONS, GROUP_LABEL } from '@/lib/api/mappers/audit-report-builder.mappers';
import type {
  FindingStatus,
  ReportClause,
  RaiseNcPayload,
} from '@/lib/api/types/audit-report-builder.types';
import type { PanelProps } from './panelTypes';

interface Draft {
  status: FindingStatus | null;
  evidence_text: string;
  docs_reviewed: string[];
}

// remembers the group the auditor was on for each report, so a reload does not jump to the first group
const lastGroupByReport = new Map<number, string>();

const toDraft = (c: ReportClause): Draft => ({
  status: c.status,
  evidence_text: c.evidence_text ?? '',
  docs_reviewed: c.docs_reviewed ?? [],
});

interface Props extends PanelProps {
  /** Called after an NC is raised so the shell can reload the model (NC list + links). */
  onReload: () => void;
}

interface Group {
  id: string;
  label: string; // heading shown in the navigator / table title
  kind: string;  // Clause Review, Risk, ...
  items: ReportClause[];
}

export default function ClausesPanel({ model, canEdit, onDirty, onReload }: Props) {
  const [drafts, setDrafts] = useState<Record<number, Draft>>(() =>
    Object.fromEntries(model.clauses.map((c) => [c.id, toDraft(c)])),
  );
  const [base, setBase] = useState<Record<number, Draft>>(() =>
    Object.fromEntries(model.clauses.map((c) => [c.id, toDraft(c)])),
  );
  const [filter, setFilter] = useState<'all' | 'open' | FindingStatus>('all');
  const [search, setSearch] = useState('');
  const [saving, setSaving] = useState(false);
  const [ncFor, setNcFor] = useState<ReportClause | null>(null);
  const [aiBusy, setAiBusy] = useState<number | 'all' | null>(null);
  const [expanded, setExpanded] = useState<Set<number>>(new Set());
  // pictures per clause row (the picture box opens in a small window)
  const [photoCounts, setPhotoCounts] = useState<Record<number, number>>({});
  const [photosFor, setPhotosFor] = useState<ReportClause | null>(null);
  // 2026-11-11: Enable / Disable per clause row. Every row is enabled by default; a disabled row is left off the printed
  // report (Word, PDF, preview) and needs no status. Saved at once as extras.disabled_clauses.
  const [disabled, setDisabled] = useState<Set<number>>(() => new Set((model.extras?.disabled_clauses ?? []).map(Number)));
  const [toggling, setToggling] = useState<number | null>(null);
  const saveDisabled = async (next: Set<number>, msg: string, busyId: number) => {
    const prev = disabled;
    setDisabled(next);
    setToggling(busyId);
    try {
      await updateReportHeader(model.report.id, { extras: { disabled_clauses: Array.from(next) } });
      toast.success(msg);
    } catch (e: any) {
      setDisabled(prev);
      toast.error(e.message || 'Could not save');
    } finally {
      setToggling(null);
    }
  };
  const toggleEnabled = (c: ReportClause) => {
    const n = new Set(disabled);
    const turnOff = !n.has(c.id);
    if (turnOff) n.add(c.id); else n.delete(c.id);
    if (turnOff && c.nc_id) toast('This clause has an NC raised — the NC still prints in the Summary of audit.', { duration: 5000 });
    saveDisabled(n, turnOff ? `Clause ${c.code} disabled — it will not appear on the report` : `Clause ${c.code} enabled — it will appear on the report`, c.id);
  };
  const setGroupEnabled = (on: boolean) => {
    if (!active) return;
    const n = new Set(disabled);
    active.items.forEach((c) => (on ? n.delete(c.id) : n.add(c.id)));
    saveDisabled(n, on ? 'All rows of this group enabled' : 'All rows of this group disabled — not on the report', -1);
  };

  const loadPhotoCounts = () => {
    listReportImages(model.report.id)
      .then((rows) => {
        const n: Record<number, number> = {};
        rows.forEach((r) => { if (r.clause_id) n[r.clause_id] = (n[r.clause_id] ?? 0) + 1; });
        setPhotoCounts(n);
      })
      .catch(() => undefined);
  };
  useEffect(() => {
    loadPhotoCounts();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [model.report.id]);

  // groups in server order
  const groups: Group[] = useMemo(() => {
    const out: Group[] = [];
    model.clauses.forEach((c) => {
      const id = `${c.group_key}|${c.heading}`;
      let g = out.find((x) => x.id === id);
      if (!g) {
        g = { id, label: c.heading || GROUP_LABEL[c.group_key] || c.group_key, kind: GROUP_LABEL[c.group_key] ?? c.group_key, items: [] };
        out.push(g);
      }
      g.items.push(c);
    });
    return out;
  }, [model.clauses]);

  const [activeId, setActiveIdState] = useState<string>(
    () => lastGroupByReport.get(model.report.id) ?? groups[0]?.id ?? '',
  );
  const setActiveId = (id: string) => {
    lastGroupByReport.set(model.report.id, id);
    setActiveIdState(id);
  };
  const activeIdx = Math.max(0, groups.findIndex((g) => g.id === activeId));
  const active = groups[activeIdx];

  /** Live counts from the drafts, so unsaved work shows in the sidebar immediately. */
  const countsIn = (g: Group) => ({
    answered: g.items.filter((c) => drafts[c.id]?.status || disabled.has(c.id)).length,
    nc: g.items.filter((c) => drafts[c.id]?.status === 'NC').length,
    ofi: g.items.filter((c) => drafts[c.id]?.status === 'OFI').length,
  });

  const isDirty = (id: number) => JSON.stringify(drafts[id]) !== JSON.stringify(base[id]);
  const dirtyIds = useMemo(
    () => model.clauses.filter((c) => isDirty(c.id)).map((c) => c.id),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [drafts, base],
  );

  const patch = (id: number, p: Partial<Draft>) => {
    setDrafts((prev) => ({ ...prev, [id]: { ...prev[id], ...p } }));
    onDirty('clauses', true);
  };

  /** C shows the original standard wording (stored in the database); any other status removes it again. */
  const setStatus = (c: ReportClause, next: FindingStatus | null) => {
    const d = drafts[c.id];
    const def = (c.default_evidence ?? '').trim();
    let evidence = d.evidence_text;
    if (next === 'C' && !evidence.trim() && def) evidence = def;
    if (next !== 'C' && def && evidence.trim() === def) evidence = '';
    patch(c.id, { status: next, evidence_text: evidence });
  };

  const answered = model.clauses.filter((c) => !disabled.has(c.id) && drafts[c.id]?.status).length;
  const total = model.clauses.filter((c) => !disabled.has(c.id)).length;
  const pct = total ? Math.round((answered / total) * 100) : 0;

  const visible = (active?.items ?? []).filter((c) => {
    const d = drafts[c.id];
    if (filter === 'open' && d.status) return false;
    if (filter !== 'all' && filter !== 'open' && d.status !== filter) return false;
    if (search.trim()) {
      const q = search.trim().toLowerCase();
      const hay = `${c.code} ${c.title} ${c.requirement ?? ''} ${d.evidence_text}`.toLowerCase();
      if (!hay.includes(q)) return false;
    }
    return true;
  });

  // ── navigation helpers ──
  const goGroup = (idx: number) => {
    const g = groups[Math.min(Math.max(idx, 0), groups.length - 1)];
    if (g) {
      setActiveId(g.id);
      if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
    }
  };

  const nextUnanswered = () => {
    const all = model.clauses;
    const startIdx = Math.max(0, all.findIndex((c) => c.id === (active?.items[0]?.id ?? 0)));
    const ordered = [...all.slice(startIdx), ...all.slice(0, startIdx)];
    const hit = ordered.find((c) => !disabled.has(c.id) && !drafts[c.id]?.status);
    if (!hit) return toast.success('Every checkpoint has a finding.');
    const g = groups.find((x) => x.items.some((i) => i.id === hit.id));
    if (g) setActiveId(g.id);
    setFilter('all');
    setSearch('');
    setTimeout(() => document.getElementById(`clause-row-${hit.id}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }), 60);
  };

  /** Set every unanswered row of this group to C (or NA) in one click. */
  const fillRest = (status: FindingStatus) => {
    if (!active) return;
    const rows = active.items.filter((c) => !disabled.has(c.id) && !drafts[c.id]?.status);
    if (!rows.length) return toast('This group is already answered.');
    setDrafts((prev) => {
      const n = { ...prev };
      rows.forEach((c) => {
        const def = status === 'C' ? (c.default_evidence ?? '').trim() : '';
        n[c.id] = { ...n[c.id], status, evidence_text: n[c.id].evidence_text.trim() ? n[c.id].evidence_text : def };
      });
      return n;
    });
    onDirty('clauses', true);
    toast.success(`${rows.length} row(s) set to ${status}. Click Save to keep them.`);
  };

  const saveAll = async () => {
    if (!dirtyIds.length) return toast('Nothing to save');
    setSaving(true);
    try {
      await saveClausesBulk(
        model.report.id,
        dirtyIds.map((id) => ({
          id,
          status: drafts[id].status,
          evidence_text: drafts[id].evidence_text,
          docs_reviewed: drafts[id].docs_reviewed,
        })),
      );
      setBase({ ...drafts });
      onDirty('clauses', false);
      toast.success(`${dirtyIds.length} clause row(s) saved`);
    } catch (e: any) {
      toast.error(e.message || 'Save failed');
    } finally {
      setSaving(false);
    }
  };

  /** Ask the AI to draft objective evidence (saved on the server). ids = one row; no ids = every empty row. */
  const runAi = async (ids?: number[]) => {
    if (!ids && dirtyIds.length) {
      toast.error('Save your clause changes first, then run the AI draft.');
      return;
    }
    setAiBusy(ids ? ids[0] : 'all');
    try {
      const res = await aiGenerateEvidence(model.report.id, { clause_ids: ids, overwrite: !!ids, mark_conforming: true });
      if (res.items.length) {
        const merge = (prev: Record<number, Draft>) => {
          const n = { ...prev };
          res.items.forEach((it) => {
            if (!n[it.id]) return;
            n[it.id] = {
              ...n[it.id],
              evidence_text: it.evidence_text,
              status: (it.status as FindingStatus | undefined) ?? n[it.id].status,
              docs_reviewed: it.docs_reviewed ?? n[it.id].docs_reviewed,
            };
          });
          return n;
        };
        setDrafts(merge);
        setBase(merge);
      }
      if (res.generated) toast.success(`AI drafted evidence for ${res.generated} row(s). Please review and edit.`);
      else if (res.skipped_findings) toast('AI does not write NC / OFI findings — write those yourself.');
      else toast('Nothing to draft.');
      if (!ids && res.skipped_findings) toast(`${res.skipped_findings} NC/OFI row(s) were skipped: write the finding yourself.`, { duration: 6000 });
    } catch (e: any) {
      toast.error(e.message || 'AI draft failed');
    } finally {
      setAiBusy(null);
    }
  };

  // ── Bold / bullet buttons: **bold** and "• " lines are printed exactly like the Word report ──
  const boxes = useRef<Record<number, HTMLTextAreaElement | null>>({});
  const formatText = (id: number, kind: 'bold' | 'bullet' | 'italic' | 'underline' | 'clear') => {
    const el = boxes.current[id];
    const cur = drafts[id].evidence_text;
    const a = el?.selectionStart ?? cur.length;
    const b = el?.selectionEnd ?? cur.length;
    let next = cur;
    let caret = b;
    // italic / underline / clear (and Ctrl+B) use the shared toggles: they keep the words selected and toggle off when re-applied
    if (kind === 'italic' || kind === 'underline' || kind === 'clear') {
      const fn = kind === 'italic' ? toggleItalic : kind === 'underline' ? toggleUnderline : clearFormatting;
      const r = fn(cur, a, b);
      patch(id, { evidence_text: r.text });
      setTimeout(() => { el?.focus(); el?.setSelectionRange(r.start, r.end); }, 0);
      return;
    }
    if (kind === 'bold') {
      const sel = cur.slice(a, b) || 'bold text';
      next = cur.slice(0, a) + '**' + sel + '**' + cur.slice(b);
      caret = a + sel.length + 4;
    } else {
      const start = cur.lastIndexOf('\n', Math.max(0, a - 1)) + 1;
      const endIdx = cur.indexOf('\n', b);
      const end = endIdx === -1 ? cur.length : endIdx;
      const block = cur.slice(start, end);
      const lines = block.split('\n');
      const allBullets = lines.every((l) => /^\s*[•\-*]\s/.test(l));
      const out = lines.map((l) => (allBullets ? l.replace(/^\s*[•\-*]\s/, '') : /^\s*[•\-*]\s/.test(l) ? l : '• ' + l)).join('\n');
      next = cur.slice(0, start) + out + cur.slice(end);
      caret = start + out.length;
    }
    patch(id, { evidence_text: next });
    setTimeout(() => { el?.focus(); el?.setSelectionRange(caret, caret); }, 0);
  };

  /** Raise needs the row saved first: save just this row, then open the form. */
  const openRaise = async (c: ReportClause) => {
    if (isDirty(c.id)) {
      try {
        const d = drafts[c.id];
        await saveClausesBulk(model.report.id, [{ id: c.id, status: d.status, evidence_text: d.evidence_text, docs_reviewed: d.docs_reviewed }]);
        setBase((prev) => ({ ...prev, [c.id]: d }));
        if (dirtyIds.filter((x) => x !== c.id).length === 0) onDirty('clauses', false);
      } catch (e: any) {
        toast.error(e.message || 'Save failed');
        return;
      }
    }
    setNcFor(c);
  };

  const toggle = (set: Set<number>, setter: (s: Set<number>) => void, id: number) => {
    const n = new Set(set);
    n.has(id) ? n.delete(id) : n.add(id);
    setter(n);
  };

  // standard headings in the navigator (combined report: "ISO 9001:2015 — Clause 4 …")
  const standardOf = (label: string) => (label.includes(' — ') ? label.split(' — ')[0] : '');
  const shortLabel = (label: string) => (label.includes(' — ') ? label.split(' — ').slice(1).join(' — ') : label);

  return (
    <div>
      <div className="rb-progress">
        <span>
          <strong>{answered}</strong> of <strong>{total}</strong> checkpoints answered
        </span>
        <div className="rb-bar">
          <span style={{ width: `${pct}%` }} />
        </div>
        <span>{pct}%</span>
      </div>

      <div className="rb-split">
        {/* ── left: group navigator with previous / select / next ── */}
        <aside className="rb-nav" aria-label="Clause groups">
          <div className="rb-nav-head">
            <div className="rb-nav-title">Clause groups ({groups.length})</div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
              <button
                type="button"
                className="rb-btn rb-btn-sm"
                disabled={activeIdx === 0}
                onClick={() => goGroup(activeIdx - 1)}
                aria-label="Previous group"
              >
                <FiChevronLeft size={12} />
              </button>
              <select
                className="rb-select"
                style={{ flex: 1, minWidth: 0 }}
                value={active?.id ?? ''}
                onChange={(e) => {
                  const idx = groups.findIndex((g) => g.id === e.target.value);
                  if (idx >= 0) goGroup(idx);
                }}
              >
                {groups.map((g) => (
                  <option key={g.id} value={g.id}>
                    {(standardOf(g.label) ? `${standardOf(g.label)} · ` : '') + shortLabel(g.label)}
                  </option>
                ))}
              </select>
              <button
                type="button"
                className="rb-btn rb-btn-sm"
                disabled={activeIdx >= groups.length - 1}
                onClick={() => goGroup(activeIdx + 1)}
                aria-label="Next group"
              >
                <FiChevronRight size={12} />
              </button>
            </div>
            <button className="rb-btn rb-btn-sm" style={{ width: '100%', justifyContent: 'center' }} onClick={nextUnanswered}>
              <FiSkipForward size={12} /> Next unanswered
            </button>
          </div>
          {groups.map((g, i) => {
            const k = countsIn(g);
            const done = k.answered === g.items.length;
            const std = standardOf(g.label);
            const prevStd = i > 0 ? standardOf(groups[i - 1].label) : '';
            return (
              <React.Fragment key={g.id}>
                {std && std !== prevStd && <div className="rb-nav-std">{std}</div>}
                <button
                  className={`rb-nav-item${g.id === active?.id ? ' is-active' : ''}${done ? ' is-done' : ''}`}
                  onClick={() => goGroup(i)}
                >
                  <span className="rb-nav-name">{shortLabel(g.label)}</span>
                  <span className="rb-nav-sub">
                    <span>{done ? <><FiCheck size={10} /> done</> : g.kind}</span>
                    <span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
                      {k.nc > 0 && (
                        <span className="rb-pill" style={{ background: '#fee2e2', color: '#991b1b', padding: '0 6px' }}>NC {k.nc}</span>
                      )}
                      {k.ofi > 0 && (
                        <span className="rb-pill" style={{ background: '#fef3c7', color: '#92400e', padding: '0 6px' }}>OFI {k.ofi}</span>
                      )}
                      <span>{k.answered}/{g.items.length}</span>
                    </span>
                  </span>
                  <span className="rb-nav-bar"><span style={{ width: `${g.items.length ? (k.answered / g.items.length) * 100 : 0}%` }} /></span>
                </button>
              </React.Fragment>
            );
          })}
        </aside>

        {/* ── right: the selected group only ── */}
        <section>
          <div className="rb-toolbar-sticky">
            <strong style={{ fontSize: 13, marginRight: 4 }}>{active ? shortLabel(active.label) : ''}</strong>
            <select className="rb-select" style={{ width: 'auto' }} value={filter} onChange={(e) => setFilter(e.target.value as any)}>
              <option value="all">All rows</option>
              <option value="open">Unanswered only</option>
              <option value="C">Conforming (C)</option>
              <option value="NC">Non-conforming (NC)</option>
              <option value="OFI">Opportunity (OFI)</option>
              <option value="NA">Not applicable (NA)</option>
            </select>
            <input className="rb-input" style={{ width: 170 }} placeholder="Search clause / text" value={search} onChange={(e) => setSearch(e.target.value)} />
            <div style={{ flex: 1 }} />
            {canEdit && (
              <>
                <button className="rb-btn rb-btn-sm" onClick={() => fillRest('C')} title="Set every unanswered row of this group to C">
                  <FiCheck size={12} /> Rest of group = C
                </button>
                <button className="rb-btn rb-btn-sm" onClick={() => fillRest('NA')} title="Set every unanswered row of this group to NA">
                  Rest = NA
                </button>
                <button className="rb-btn rb-btn-sm" disabled={toggling !== null} onClick={() => setGroupEnabled(true)} title="Show every row of this group on the report">
                  Enable all
                </button>
                <button className="rb-btn rb-btn-sm" disabled={toggling !== null} onClick={() => setGroupEnabled(false)} title="Leave every row of this group off the report">
                  Disable all
                </button>
                <button className="rb-btn rb-btn-sm" onClick={() => runAi()} disabled={aiBusy !== null} title="Drafts objective evidence for every row that has none (conforming rows only). You review and edit.">
                  <FiZap size={12} /> {aiBusy === 'all' ? 'AI is writing…' : 'AI draft evidence'}
                </button>
                <button className="rb-btn rb-btn-primary rb-btn-sm" onClick={saveAll} disabled={saving || !dirtyIds.length}>
                  <FiSave size={12} /> {saving ? 'Saving…' : `Save${dirtyIds.length ? ` (${dirtyIds.length})` : ''}`}
                </button>
              </>
            )}
          </div>

          <div className="rb-table-wrap">
            <table className="rb-table" style={{ tableLayout: 'fixed', minWidth: 760 }}>
              <colgroup>
                <col style={{ width: 62 }} />
                <col style={{ width: 250 }} />
                <col style={{ width: 160 }} />
                <col />
                <col style={{ width: 96 }} />
                <col style={{ width: 92 }} />
              </colgroup>
              <thead>
                <tr>
                  <th>Clause</th>
                  <th>Requirement</th>
                  <th>Finding</th>
                  <th>Evidence</th>
                  <th>NC</th>
                  <th title="Enabled rows print on the report; disabled rows are left off">On report</th>
                </tr>
              </thead>
              <tbody>
                {visible.length === 0 && (
                  <tr>
                    <td colSpan={6}>
                      <div className="rb-empty">No rows match this filter in this group.</div>
                    </td>
                  </tr>
                )}
                {visible.map((c) => {
                  const d = drafts[c.id];
                  const needsNc = (d.status === 'NC' || d.status === 'OFI') && !c.nc_id;
                  const rowCls = d.status === 'NC' ? 'rb-row-nc' : d.status === 'OFI' ? 'rb-row-ofi' : d.status ? 'rb-row-done' : '';
                  const longReq = (c.requirement ?? '').length > 120;
                  const isOpen = expanded.has(c.id);
                  const off = disabled.has(c.id);
                  return (
                    <tr id={`clause-row-${c.id}`} key={c.id} className={rowCls} style={{ background: off ? '#f3f4f6' : isDirty(c.id) ? '#fffdf5' : undefined, opacity: off ? 0.55 : undefined }}>
                      <td>
                        <span className="rb-clause-code">{c.code}</span>
                      </td>
                      <td>
                        {c.title && <div style={{ fontWeight: 600, color: '#111827', fontSize: 12 }}>{c.title}</div>}
                        {c.requirement && (
                          <>
                            <div className={`rb-req${longReq && !isOpen ? ' rb-clamp' : ''}`}>{c.requirement}</div>
                            {longReq && (
                              <button className="rb-more" onClick={() => toggle(expanded, setExpanded, c.id)}>
                                {isOpen ? 'less' : 'more'}
                              </button>
                            )}
                          </>
                        )}
                      </td>
                      <td>
                        <div className="rb-seg">
                          {FINDING_OPTIONS.map((o) => (
                            <button
                              key={o.value}
                              type="button"
                              disabled={!canEdit}
                              className={d.status === o.value ? 'on' : ''}
                              style={{ ['--b' as any]: o.bg, ['--c' as any]: o.color, ['--bd' as any]: o.border }}
                              onClick={() => setStatus(c, d.status === o.value ? null : o.value)}
                            >
                              {o.label}
                            </button>
                          ))}
                        </div>
                      </td>
                      <td>
                        {canEdit && (
                          <div style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
                            <button type="button" className="rb-btn rb-btn-sm" style={{ padding: '2px 8px', fontWeight: 800 }} title="Bold the selected words (**text**) — Ctrl+B" onClick={() => formatText(c.id, 'bold')}>B</button>
                            <button type="button" className="rb-btn rb-btn-sm" style={{ padding: '2px 8px', fontStyle: 'italic', fontFamily: 'Georgia, serif' }} title="Italic the selected words (*text*) — Ctrl+I" onMouseDown={(e) => e.preventDefault()} onClick={() => formatText(c.id, 'italic')}>I</button>
                            <button type="button" className="rb-btn rb-btn-sm" style={{ padding: '2px 8px', textDecoration: 'underline' }} title="Underline the selected words (__text__) — Ctrl+U" onMouseDown={(e) => e.preventDefault()} onClick={() => formatText(c.id, 'underline')}>U</button>
                            <button type="button" className="rb-btn rb-btn-sm" style={{ padding: '2px 8px' }} title="Bullet list (• text)" onClick={() => formatText(c.id, 'bullet')}><FiList size={12} /></button>
                            <button type="button" className="rb-btn rb-btn-sm" style={{ padding: '2px 8px', fontSize: 11 }} title="Remove bold / italic / underline from the selected words (or the whole box when nothing is selected)" onMouseDown={(e) => e.preventDefault()} onClick={() => formatText(c.id, 'clear')}>Clear</button>
                          </div>
                        )}
                        <textarea
                          ref={(el) => { boxes.current[c.id] = el; }}
                          className="rb-textarea"
                          rows={2}
                          style={{ minHeight: 52 }}
                          disabled={!canEdit}
                          value={d.evidence_text}
                          placeholder={d.status === 'NC' || d.status === 'OFI' ? 'Write the finding, then click Raise to record it in the NC module' : 'Click C to insert the standard wording, or write what you saw'}
                          onChange={(e) => patch(c.id, { evidence_text: e.target.value })}
                          onKeyDown={(e) => {
                            const k = shortcutOf(e);
                            if (!k || !canEdit) return;
                            e.preventDefault();
                            if (k === 'bold') {
                              const r = toggleBold(d.evidence_text, e.currentTarget.selectionStart, e.currentTarget.selectionEnd);
                              const el = e.currentTarget;
                              patch(c.id, { evidence_text: r.text });
                              setTimeout(() => { el.focus(); el.setSelectionRange(r.start, r.end); }, 0);
                            } else formatText(c.id, k);
                          }}
                        />
                        <RichPreview text={d.evidence_text} />
                        {c.ncr_entry_id ? (() => {
                          const pos = model.ncs.findIndex((n) => n.entry_id === c.ncr_entry_id);
                          const nc = pos >= 0 ? model.ncs[pos] : null;
                          const isObs = nc ? /observation/i.test(nc.nc_type) : d.status === 'OFI';
                          const seq = nc ? model.ncs.filter((n, i) => i <= pos && /observation/i.test(n.nc_type) === isObs).length : 0;
                          return nc ? (
                            <div className="rb-opt-note" style={{ margin: '6px 0 0', padding: '6px 8px', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 8 }}>
                              <strong>From the NC module · {isObs ? 'OFI' : 'NC'}#{seq} – Clause {c.code}:</strong> {nc.ncr_statement}
                              <div style={{ color: '#6b7280' }}>The report prints this text from the NC module with the clause number.</div>
                            </div>
                          ) : null;
                        })() : null}
                        <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
                          {canEdit && d.status !== 'NC' && d.status !== 'OFI' && (
                            <button type="button" className="rb-doc-toggle" disabled={aiBusy !== null} onClick={() => runAi([c.id])}>
                              <FiZap size={10} /> {aiBusy === c.id ? 'Writing…' : 'AI draft'}
                            </button>
                          )}
                          <button type="button" className="rb-doc-toggle" onClick={() => setPhotosFor(c)}>
                            <FiImage size={10} /> Photos ({photoCounts[c.id] ?? 0})
                          </button>
                        </div>
                      </td>
                      <td>
                        {c.nc_id ? (
                          <span className="rb-pill" style={{ background: '#fee2e2', color: '#991b1b' }}>
                            <FiCheckCircle size={10} /> NC #{c.nc_id}
                          </span>
                        ) : needsNc && canEdit ? (
                          <button
                            className="rb-btn rb-btn-red rb-btn-sm"
                            title="Saves this row and records it in the NC module"
                            onClick={() => openRaise(c)}
                          >
                            <FiAlertTriangle size={11} /> Raise
                          </button>
                        ) : (
                          <span style={{ color: '#9ca3af' }}>—</span>
                        )}
                      </td>
                      <td style={{ opacity: 1 }}>
                        <button
                          type="button"
                          role="switch"
                          aria-checked={!off}
                          disabled={!canEdit || toggling !== null}
                          onClick={() => toggleEnabled(c)}
                          title={off ? 'Disabled: this clause is NOT printed on the report. Click to enable.' : 'Enabled: this clause is printed on the report. Click to disable.'}
                          style={{
                            display: 'inline-flex', alignItems: 'center', gap: 6, border: 'none', background: 'transparent',
                            cursor: canEdit ? 'pointer' : 'default', padding: 0, fontSize: 11, fontWeight: 600,
                            color: off ? '#6b7280' : '#166534',
                          }}
                        >
                          <span style={{ position: 'relative', width: 32, height: 18, borderRadius: 9, background: off ? '#d1d5db' : '#16a34a', transition: 'background .15s' }}>
                            <span style={{ position: 'absolute', top: 2, left: off ? 2 : 16, width: 14, height: 14, borderRadius: '50%', background: '#fff', transition: 'left .15s' }} />
                          </span>
                          {off ? 'Disabled' : 'Enabled'}
                        </button>
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>

          <div className="rb-pager">
            <button className="rb-btn" disabled={activeIdx === 0} onClick={() => goGroup(activeIdx - 1)}>
              <FiChevronLeft size={14} /> {activeIdx > 0 ? shortLabel(groups[activeIdx - 1].label) : 'Previous'}
            </button>
            <button className="rb-btn rb-btn-primary" disabled={activeIdx >= groups.length - 1} onClick={() => goGroup(activeIdx + 1)}>
              {activeIdx < groups.length - 1 ? shortLabel(groups[activeIdx + 1].label) : 'Next'} <FiChevronRight size={14} />
            </button>
          </div>
        </section>
      </div>

      {photosFor && (
        <div className="rb-modal-bg" onClick={() => { setPhotosFor(null); loadPhotoCounts(); }}>
          <div className="rb-modal" style={{ maxWidth: 760 }} onClick={(e) => e.stopPropagation()}>
            <h3>Photos — clause {photosFor.code}</h3>
            <p>They print inside this clause row&apos;s evidence cell.</p>
            <ImagesManager reportId={model.report.id} canEdit={canEdit} sectionKey="clause" clauseId={photosFor.id} />
            <div className="rb-savebar" style={{ marginTop: 14 }}>
              <button className="rb-btn rb-btn-primary" onClick={() => { setPhotosFor(null); loadPhotoCounts(); }}>Done</button>
            </div>
          </div>
        </div>
      )}

      {ncFor && (
        <RaiseNcModal
          clause={ncFor}
          evidence={drafts[ncFor.id]?.evidence_text ?? ''}
          defaultType={drafts[ncFor.id]?.status === 'OFI' ? 'Observation' : 'Minor'}
          onClose={() => setNcFor(null)}
          onDone={() => {
            setNcFor(null);
            onReload();
          }}
          reportId={model.report.id}
        />
      )}
    </div>
  );
}

function RaiseNcModal({
  clause,
  evidence,
  defaultType,
  reportId,
  onClose,
  onDone,
}: {
  clause: ReportClause;
  evidence: string;
  defaultType: RaiseNcPayload['nc_type'];
  reportId: number;
  onClose: () => void;
  onDone: () => void;
}) {
  const [type, setType] = useState<RaiseNcPayload['nc_type']>(defaultType);
  const [statement, setStatement] = useState(evidence);
  const [action, setAction] = useState('');
  const [busy, setBusy] = useState(false);
  const [aiBusy, setAiBusy] = useState(false);

  const aiWord = async () => {
    setAiBusy(true);
    try {
      const r = await aiGenerateFinding(reportId, { clause_id: clause.id, note: statement, nc_type: type });
      setStatement(r.statement);
      toast.success('AI worded the finding. Please check it.');
    } catch (e: any) {
      toast.error(e.message || 'AI could not word the finding');
    } finally {
      setAiBusy(false);
    }
  };

  const submit = async () => {
    setBusy(true);
    try {
      await raiseClauseNc(reportId, clause.id, {
        nc_type: type,
        statement: statement.trim() || undefined,
        corrective_action: action.trim() || undefined,
      });
      toast.success(`${type} raised for clause ${clause.code}`);
      onDone();
    } catch (e: any) {
      toast.error(e.message || 'Could not raise NC');
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="rb-modal-bg" onClick={onClose}>
      <div className="rb-modal" onClick={(e) => e.stopPropagation()}>
        <h3>Raise NC — clause {clause.code}</h3>
        <p>Creates the entry in the NC module and links it to this clause row.</p>
        <div className="rb-field" style={{ marginBottom: 12 }}>
          <label className="rb-label">Type</label>
          <select className="rb-select" value={type} onChange={(e) => setType(e.target.value as any)}>
            <option value="Major">Major</option>
            <option value="Minor">Minor</option>
            <option value="Observation">Observation</option>
          </select>
        </div>
        <div className="rb-field" style={{ marginBottom: 12 }}>
          <label className="rb-label">Statement of the finding</label>
          <textarea className="rb-textarea" rows={4} value={statement} onChange={(e) => setStatement(e.target.value)} placeholder="What did you find? A short note is enough." />
          <div>
            <button type="button" className="rb-btn rb-btn-sm" style={{ marginTop: 6 }} disabled={aiBusy || !statement.trim()} onClick={aiWord}>
              <FiZap size={11} /> {aiBusy ? 'Wording…' : 'AI: word it formally'}
            </button>
          </div>
        </div>
        <div className="rb-field" style={{ marginBottom: 16 }}>
          <label className="rb-label">Corrective action (optional)</label>
          <textarea className="rb-textarea" rows={2} value={action} onChange={(e) => setAction(e.target.value)} />
        </div>
        <div className="rb-savebar">
          <button className="rb-btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="rb-btn rb-btn-red" onClick={submit} disabled={busy || !(statement.trim() || evidence.trim())}>
            {busy ? 'Raising…' : 'Raise NC'}
          </button>
        </div>
      </div>
    </div>
  );
}