/* ============================================================
   CHANGE MANAGEMENT MODULE
   ============================================================ */

const NEW_CHANGE = (seed) => ({
  id:'', title:'', description:'', changeType:'Normal', risk:'Medium', impact:'Medium',
  owner:null, implementer:null, requesterPersonId:'', reason:'',
  implementationPlan:'', backoutPlan:'', testPlan:'', communicationsPlan:'',
  plannedStart:'', plannedEnd:'', actualStart:'', actualEnd:'',
  outageRequired:false, outageMinutes:0, reviewOutcome:'', reviewNotes:'',
  componentIds:[], requestIds:[], ...(seed || {})
});

const ChangeType = ({value}) => <span className={'ctype ctype-'+value}>{value}</span>;
const Risk = ({value}) => <span className={'risk risk-'+value} title={value + ' risk'}>{value}</span>;

/** Datetime-local wants "YYYY-MM-DDTHH:MM"; the API stores full ISO strings. */
const toLocalInput = (iso) => {
  if (!iso) return '';
  const d = new Date(iso);
  if (isNaN(d)) return '';
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const fromLocalInput = (value) => value ? new Date(value).toISOString() : '';
const fmtWindow = (start, end) => {
  if (!start) return 'Not scheduled';
  const s = new Date(start), e = end ? new Date(end) : null;
  const date = s.toLocaleDateString('en-GB', {day:'numeric', month:'short', year:'numeric'});
  const from = s.toLocaleTimeString('en-GB', {hour:'2-digit', minute:'2-digit'});
  if (!e) return `${date} at ${from}`;
  const sameDay = s.toDateString() === e.toDateString();
  const to = e.toLocaleTimeString('en-GB', {hour:'2-digit', minute:'2-digit'});
  return sameDay ? `${date}, ${from} to ${to}`
    : `${date} ${from} → ${e.toLocaleDateString('en-GB',{day:'numeric',month:'short'})} ${to}`;
};

/** The workflow as a progress bar, so people can see where a change has got to. */
function ChangeStepper({change, state}){
  const path = state.changeHappyPath || [];
  const failed = ['Rejected','Cancelled','Failed'].includes(change.status);
  const index = path.indexOf(change.status);
  return (
    <div className="stepper">
      {path.map((step, i) => (
        <React.Fragment key={step}>
          {i > 0 && <span className={'step-line' + (index > i - 1 && !failed ? ' done' : '')}/>}
          <span className={'step' + (
            failed ? ' off'
            : i < index ? ' done'
            : i === index ? ' current' : ' off')}>
            <span className="bullet">{i < index && !failed ? <I.check size={11}/> : i + 1}</span>
            {step}
          </span>
        </React.Fragment>
      ))}
      {failed && <span className="step current" style={{marginLeft:10}}>
        <span className="bullet" style={{borderColor:'var(--red)',background:'var(--red)',color:'#fff'}}>
          <I.x size={11}/></span>{change.status}</span>}
    </div>
  );
}

/* ----------------------------------------------------------------- module */

function ChangesModule({state, idx, go, act, toast, user, route}){
  const [view, setView] = useState(()=>urlParam('view','list'));
  const [q, setQ] = useState(()=>urlParam('q'));
  const [fType, setFType] = useState(()=>urlParam('type'));
  const [fRisk, setFRisk] = useState(()=>urlParam('risk'));
  const [fStatus, setFStatus] = useState(()=>urlParam('status'));
  const [mine, setMine] = useState(()=>urlParam('mine')==='1');
  const [showClosed, setShowClosed] = useState(()=>urlParam('closed')==='1');
  const [editing, setEditing] = useState(null);
  const onOpen = (c) => go({view:'change', id:c.id});
  const {menu, openMenu, closeMenu} = useRecordMenu();
  const onContextMenu = (e, c) => openMenu(e, recordMenuItem('change', c));
  const bulk = useBulkSelect();
  const [bulkOwnerVal, setBulkOwnerVal] = useState('');
  useEffect(()=>{ bulk.clear(); }, [view]);

  const canEdit = (id) => {
    const ids = state.permissions.editableChangeIds;
    return ids === '*' || (Array.isArray(ids) && ids.includes(id));
  };
  const awaiting = state.permissions.changesAwaitingMyApproval || [];

  useEffect(()=>{
    if (!route) return;
    if (route.action === 'new') setEditing(NEW_CHANGE());
    if (route.filter === 'mine') setMine(true);
    if (route.subview) setView(route.subview);
  }, [route]);

  useFilterUrlSync({
    view:[view,'list'], q:[q,''], type:[fType,''], risk:[fRisk,''], status:[fStatus,''],
    mine:[mine?'1':'',''], closed:[showClosed?'1':'','']
  });

  const {sort, sorted, Th} = useSort('planned', {
    reference: c => c.reference, title: c => c.title, type: c => c.changeType, risk: c => c.risk,
    status: c => c.status, planned: c => c.plannedStart || '9999',
    owner: c => partyName(c.owner, idx) || 'zzz'
  });

  const clearFilters = () => { setQ('');setFType('');setFRisk('');setFStatus('');setMine(false); };
  const hasFilters = !!(q||fType||fRisk||fStatus||mine);

  const closed = (c) => ['Closed','Cancelled','Rejected'].includes(c.status);
  const rows = useMemo(()=>{
    const term = q.trim().toLowerCase();
    const list = state.changes.filter(c => {
      if (!showClosed && closed(c)) return false;
      if (fType && c.changeType !== fType) return false;
      if (fRisk && c.risk !== fRisk) return false;
      if (fStatus && c.status !== fStatus) return false;
      if (mine && !awaiting.includes(c.id) && !canEdit(c.id)) return false;
      if (!term) return true;
      return [c.reference, c.title, c.description, c.reason, c.changeType,
        partyName(c.owner, idx), partyName(c.implementer, idx),
        (c.componentIds||[]).map(cid => (idx.comp[cid]||{}).name).join(' ')
      ].join(' ').toLowerCase().includes(term);
    });
    return view === 'list' ? sorted(list) : list;
  }, [state.changes, q, fType, fRisk, fStatus, mine, showClosed, idx, awaiting, view, sort]);

  const bulkApplyOwner = async () => {
    if (!bulkOwnerVal) return;
    const party = parsePartyKey(bulkOwnerVal);
    const ids = [...bulk.selected];
    try {
      for (const id of ids) {
        const c = state.changes.find(x => x.id === id);
        if (c) await API.put(`/api/changes/${id}`, {...c, owner: party});
      }
      await act(Promise.resolve(), `Reassigned ${ids.length} change${ids.length===1?'':'s'}`);
    } catch (err) { toast(err.message, true); }
    finally { bulk.clear(); setBulkOwnerVal(''); }
  };
  const bulkExportSelected = () => {
    const chosen = rows.filter(c => bulk.selected.has(c.id));
    exportCsv('fuse-changes-selected.csv',
      ['Reference','Title','Type','Risk','Impact','Status','Owner','Implementer','Planned start','Planned end'],
      chosen.map(c => [c.reference, c.title, c.changeType, c.risk, c.impact, c.status,
        partyName(c.owner, idx), partyName(c.implementer, idx), c.plannedStart, c.plannedEnd]));
  };

  const openCount = state.changes.filter(c => !closed(c)).length;
  const upcoming = state.changes.filter(c =>
    ['Approved','Scheduled'].includes(c.status) && c.plannedStart &&
    new Date(c.plannedStart) > new Date() &&
    new Date(c.plannedStart) < new Date(Date.now() + 7*86400000));

  const exportList = () => exportCsv('fuse-changes.csv',
    ['Reference','Title','Type','Risk','Impact','Status','Owner','Implementer','Planned start',
     'Planned end','Outage minutes','Components','Business requests','Approvals decided','Review outcome'],
    rows.map(c => [c.reference, c.title, c.changeType, c.risk, c.impact, c.status,
      partyName(c.owner, idx), partyName(c.implementer, idx), c.plannedStart, c.plannedEnd,
      c.outageMinutes,
      (c.componentIds||[]).map(cid => (idx.comp[cid]||{}).identifier).filter(Boolean).join(' '),
      (c.requestIds||[]).map(rid => (idx.request[rid]||{}).reference).filter(Boolean).join(' '),
      `${c.approvalState.decided}/${c.approvalState.total}`, c.reviewOutcome]));

  const ownerOptions = [
    ...state.people.map(p=>({k:'person:'+p.id, label:p.name})),
    ...state.roles.map(r=>({k:'role:'+r.id, label:r.name + ' (role)'}))
  ].sort((a,b)=>a.label.localeCompare(b.label));

  const bulkBar = rows.length > 0 &&
    <BulkBar count={bulk.selected.size} onClear={bulk.clear}>
      <select style={{width:'auto',maxWidth:200}} value={bulkOwnerVal} onChange={e=>setBulkOwnerVal(e.target.value)}>
        <option value="">Reassign owner to…</option>
        {ownerOptions.map(o=><option key={o.k} value={o.k}>{o.label}</option>)}
      </select>
      <button className="btn sm" disabled={!bulkOwnerVal} onClick={bulkApplyOwner}>Apply</button>
      <button className="btn sm" onClick={bulkExportSelected}><I.down size={13}/>Export selected</button>
    </BulkBar>;

  const shared = {changes: rows, state, idx, go, act, user, onOpen, onContextMenu, awaiting, hasFilters, clearFilters};

  return (
    <div className="mod">
      <div className="mod-head">
        {awaiting.length > 0 &&
          <div className="warn-box row" style={{gap:10}}>
            <I.stamp size={16}/>
            <span style={{flex:1}}>
              {awaiting.length === 1
                ? 'One change is waiting for your approval.'
                : `${awaiting.length} changes are waiting for your approval.`}
            </span>
            <button className="btn sm" onClick={()=>{ setMine(true); setFStatus('Awaiting approval'); }}>
              Show them</button>
          </div>}

        <div className="stats-row">
          <div className="stat"><div className="n">{openCount}</div><div className="l">Open</div></div>
          <div className="stat"><div className="n" style={{color: awaiting.length ? 'var(--red)' : undefined}}>
            {awaiting.length}</div><div className="l">Need your decision</div></div>
          <div className="stat"><div className="n">{state.changes.filter(c=>c.status==='Awaiting approval').length}</div>
            <div className="l">Awaiting approval</div></div>
          <div className="stat"><div className="n">{upcoming.length}</div><div className="l">Next 7 days</div></div>
          <div className="stat"><div className="n">{state.changes.filter(c=>c.changeType==='Emergency').length}</div>
            <div className="l">Emergency</div></div>
        </div>

        <div className="row wrap">
          <ViewSwitch value={view} onChange={setView} views={[
            {k:'list', label:'List', icon:<I.list size={13}/>},
            {k:'board', label:'Board', icon:<I.board size={13}/>},
            {k:'calendar', label:'Calendar', icon:<I.calendar size={13}/>},
            {k:'analytics', label:'Analytics', icon:<I.chart size={13}/>}
          ]}/>
          <div className="spacer"/>
          <button className="btn" onClick={exportList}><I.down size={14}/>CSV</button>
        </div>

        {view !== 'analytics' &&
          <div className="row wrap">
            <SearchBox value={q} onChange={setQ} aria="Search changes"
                       title="Search reference, title or component"/>
            <select style={{width:'auto'}} value={fType} onChange={e=>setFType(e.target.value)}>
              <option value="">All types</option>
              {(state.changeTypes||[]).map(t=><option key={t}>{t}</option>)}</select>
            <select style={{width:'auto'}} value={fRisk} onChange={e=>setFRisk(e.target.value)}>
              <option value="">Any risk</option>
              {(state.changeRisks||[]).map(t=><option key={t}>{t}</option>)}</select>
            {view !== 'board' &&
              <select style={{width:'auto'}} value={fStatus} onChange={e=>setFStatus(e.target.value)}>
                <option value="">All statuses</option>
                {(state.changeStatuses||[]).map(t=><option key={t}>{t}</option>)}</select>}
            <button className={'chip-toggle'+(mine?' on':'')} onClick={()=>setMine(v=>!v)}>Mine</button>
            <button className={'chip-toggle'+(showClosed?' on':'')} onClick={()=>setShowClosed(v=>!v)}>
              Include closed</button>
            {hasFilters &&
              <button className="btn ghost sm" onClick={clearFilters}>Clear</button>}
            <div className="spacer"/>
            <span className="xsmall dim">{rows.length} shown</span>
          </div>}
      </div>

      <div className="mod-body">
        {view === 'list' &&
          <ChangeList {...shared} Th={Th} selected={bulk.selected} onToggle={bulk.toggle}
            onToggleAll={bulk.toggleAll} bulkBar={bulkBar}/>}
        {view === 'board' && <ChangeBoard {...shared} statuses={state.changeOpenStatuses || []}/>}
        {view === 'calendar' && <div className="mod-scroll"><ChangeCalendar {...shared} freezeWindows={state.freezeWindows}/></div>}
        {view === 'analytics' && <div className="mod-scroll"><ChangesAnalytics state={state} idx={idx} go={go}/></div>}
      </div>

      <RecordContextMenu menu={menu} onClose={closeMenu} toast={toast}/>

      {editing &&
        <ChangeForm value={editing} state={state} onClose={()=>setEditing(null)} toast={toast}
          onSave={async (v)=>{
            const isNew = !v.id;
            const ok = await act(isNew ? API.post('/api/changes', v) : API.put(`/api/changes/${v.id}`, v),
              isNew ? 'Change raised' : 'Change saved');
            if (ok) setEditing(null);
          }}/>}
    </div>
  );
}

/* --------------------------------------------------------------- analytics */

function ChangesAnalytics({state, idx, go}){
  const changes = state.changes;
  const isClosedChange = (c) => ['Closed','Cancelled','Rejected'].includes(c.status);
  const open = changes.filter(c => !isClosedChange(c));

  const byStatus = (state.changeStatuses||[]).map(s => ({k:s, n: changes.filter(c=>c.status===s).length}));
  const byRisk = (state.changeRisks||[]).map(r => ({k:r, n: open.filter(c=>c.risk===r).length}));
  const byType = (state.changeTypes||[]).map(t => ({k:t, n: open.filter(c=>c.changeType===t).length}));

  const approvalTotals = changes.reduce((s,c) => {
    s.decided += c.approvalState.decided; s.total += c.approvalState.total; return s;
  }, {decided:0, total:0});
  const awaitingDecision = changes.filter(c => c.status === 'Awaiting approval');

  // Freeze window collisions: an open change whose planned window overlaps an active freeze window.
  const freezeWindows = (state.freezeWindows||[]).filter(f => f.isActive);
  const collisions = open.filter(c => c.plannedStart && freezeWindows.some(f => {
    const cs = new Date(c.plannedStart), ce = new Date(c.plannedEnd || c.plannedStart);
    const fs = new Date(f.startsAt), fe = new Date(f.endsAt);
    return cs <= fe && ce >= fs;
  }));

  const byOwner = {};
  open.forEach(c => {
    const p = resolveParty(c.owner, idx);
    const k = p ? p.name : 'Unassigned';
    byOwner[k] = (byOwner[k]||0) + 1;
  });
  const ownerRows = Object.entries(byOwner).sort((a,b)=>b[1]-a[1]).slice(0,8);

  const now = new Date();
  const monthKeys = [];
  for (let i = 5; i >= 0; i--){
    const d = new Date(now.getFullYear(), now.getMonth()-i, 1);
    monthKeys.push({label: d.toLocaleDateString(undefined,{month:'short',year:'2-digit'}),
      ym: d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')});
  }
  const plannedTrend = monthKeys.map(m => ({...m, n: changes.filter(c => (c.plannedStart||'').slice(0,7) === m.ym).length}));
  const maxTrend = Math.max(1, ...plannedTrend.map(m=>m.n));

  const Bar = ({n, max, tone}) =>
    <span className="bar" style={{marginTop:6}}>
      <span style={{width:(max?n/max*100:0)+'%', background: tone || 'var(--accent)'}}/></span>;

  return (
    <div className="stack">
      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit,minmax(360px,1fr))'}}>
        <div className="card">
          <div className="card-head"><h3>By status</h3><Badge tone="slate">{changes.length}</Badge></div>
          <div className="card-body tight">
            {byStatus.map(({k,n}) =>
              <div className="side-row" key={k}>
                <span style={{minWidth:0,flex:1}}>
                  <span style={{fontWeight:600}}>{k}</span>
                  <Bar n={n} max={changes.length}/>
                </span>
                <strong className="small">{n}</strong>
              </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Open changes by risk</h3><Badge tone="slate">{open.length}</Badge></div>
          <div className="card-body tight">
            {byRisk.length === 0
              ? <EmptyState icon={<I.change size={26}/>} title="Nothing open"/>
              : byRisk.map(({k,n}) =>
                  <div className="side-row" key={k}>
                    <span style={{minWidth:0,flex:1}}>
                      <span style={{fontWeight:600}}><Risk value={k}/></span>
                      <Bar n={n} max={open.length}
                        tone={k==='High' ? 'var(--red)' : k==='Medium' ? 'var(--amber)' : 'var(--green)'}/>
                    </span>
                    <strong className="small">{n}</strong>
                  </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Open changes by type</h3></div>
          <div className="card-body tight">
            {byType.length === 0
              ? <EmptyState icon={<I.change size={26}/>} title="Nothing open"/>
              : byType.map(({k,n}) =>
                  <div className="side-row" key={k}>
                    <span style={{minWidth:0,flex:1}}>
                      <span style={{fontWeight:600}}><ChangeType value={k}/></span>
                      <Bar n={n} max={open.length} tone={k==='Emergency' ? 'var(--red)' : undefined}/>
                    </span>
                    <strong className="small">{n}</strong>
                  </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Approval throughput</h3>
            <Badge tone={awaitingDecision.length ? 'amber' : 'green'}>{awaitingDecision.length} awaiting</Badge></div>
          <div className="card-body tight">
            <div className="side-row">
              <span style={{minWidth:0,flex:1}}>
                <span style={{fontWeight:600}}>Approval decisions recorded</span>
                <Bar n={approvalTotals.decided} max={approvalTotals.total || 1}/>
              </span>
              <strong className="small">{approvalTotals.decided}/{approvalTotals.total}</strong>
            </div>
            {awaitingDecision.length > 0 &&
              <div className="card-body" style={{paddingTop:8}}>
                {awaitingDecision.slice(0,6).map(c =>
                  <div key={c.id} className="xsmall dim clickable" style={{padding:'4px 0'}}
                       onClick={()=>go({view:'change', id:c.id})}>
                    <span className="mono">{c.reference}</span> {c.title} —
                    {' '}{c.approvalState.decided}/{c.approvalState.total} decided
                  </div>)}
                {awaitingDecision.length > 6 && <div className="xsmall dim">+{awaitingDecision.length-6} more</div>}
              </div>}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Freeze window collisions</h3>
            <Badge tone={collisions.length ? 'red' : 'green'}>{collisions.length}</Badge></div>
          <div className="card-body tight">
            {collisions.length === 0
              ? <EmptyState icon={<I.check size={26}/>} title="No open changes clash with a freeze window"/>
              : collisions.map(c =>
                  <div key={c.id} className="side-row clickable" onClick={()=>go({view:'change', id:c.id})}>
                    <span style={{minWidth:0,flex:1}}>
                      <span style={{fontWeight:600}}>{c.title}</span>
                      <span className="xsmall dim" style={{display:'block'}}>
                        <span className="mono">{c.reference}</span> · {fmtWindow(c.plannedStart, c.plannedEnd)}</span>
                    </span>
                    <Risk value={c.risk}/>
                  </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Busiest owners</h3></div>
          <div className="card-body tight">
            {ownerRows.length === 0
              ? <EmptyState icon={<I.users size={26}/>} title="Nothing open"/>
              : ownerRows.map(([name,n]) =>
                  <div className="side-row" key={name}>
                    <span style={{minWidth:0,flex:1}}>
                      <span style={{fontWeight:600}}>{name}</span>
                      <Bar n={n} max={ownerRows[0][1]}/>
                    </span>
                    <strong className="small">{n}</strong>
                  </div>)}
          </div>
        </div>
      </div>

      <div className="card">
        <div className="card-head"><h3>Planned per month</h3>
          <span className="xsmall dim">Last 6 months, by planned start date</span></div>
        <div className="card-body tight">
          {plannedTrend.map(m =>
            <div className="side-row" key={m.ym}>
              <span style={{minWidth:60}}>{m.label}</span>
              <span style={{flex:1}}><Bar n={m.n} max={maxTrend}/></span>
              <strong className="small">{m.n}</strong>
            </div>)}
        </div>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------------ views */

function ChangeList({changes, idx, onOpen, onContextMenu, awaiting, Th, selected, onToggle, onToggleAll,
                      hasFilters, clearFilters, bulkBar}){
  if (!changes.length) return <div className="card">
    <EmptyState icon={<I.change size={28}/>} title="No changes match"
      action={hasFilters ? <button className="btn sm" onClick={clearFilters}>Clear filters</button> : null}/></div>;
  const Head = Th || ((props) => {
    const rest = Object.assign({}, props);
    delete rest.label;
    return <th {...rest}>{props.label}</th>;
  });
  const bulk = !!selected;
  return (
    <div className="card">
      {bulkBar}
      <div className="tbl-wrap">
        <table>
          <thead><tr>
            {bulk && <th style={{width:34}}>
              <input type="checkbox" aria-label="Select all shown"
                checked={changes.length > 0 && changes.every(c=>selected.has(c.id))}
                onChange={()=>onToggleAll(changes.map(c=>c.id))}/></th>}
            <Head k="reference" label="Ref" style={{width:100}}/>
            <Head k="title" label="Change"/>
            <Head k="type" label="Type"/>
            <Head k="risk" label="Risk"/>
            <Head k="status" label="Status"/>
            <th>Approvals</th>
            <Head k="planned" label="Planned window"/>
            <Head k="owner" label="Owner"/>
          </tr></thead>
          <tbody>
            {changes.map(c => {
              const owner = resolveParty(c.owner, idx);
              const needsMe = awaiting.includes(c.id);
              return (
                <tr key={c.id} className="clickable" onClick={()=>onOpen(c)}
                    onContextMenu={(e)=>onContextMenu && onContextMenu(e, c)}>
                  {bulk && <td onClick={e=>e.stopPropagation()}>
                    <input type="checkbox" aria-label={`Select ${c.reference}`}
                      checked={selected.has(c.id)} onChange={()=>onToggle(c.id)}/></td>}
                  <td className="mono dim">{c.reference}</td>
                  <td>
                    <div style={{fontWeight:600}}>
                      {c.title}
                      {needsMe && <Badge tone="red" >Your approval</Badge>}
                    </div>
                    <div className="cell-sub">
                      {(c.componentIds||[]).map(cid => (idx.comp[cid]||{}).identifier).filter(Boolean).join(', ')
                        || 'No components linked'}
                      {(c.requestIds||[]).length > 0 &&
                        ' · ' + c.requestIds.map(rid => (idx.request[rid]||{}).reference).filter(Boolean).join(', ')}
                    </div>
                  </td>
                  <td><ChangeType value={c.changeType}/></td>
                  <td><Risk value={c.risk}/></td>
                  <td><Badge tone={CHANGE_STATUS_TONE[c.status] || 'slate'} dot>{c.status}</Badge></td>
                  <td className="small mono">
                    {c.approvalState.total
                      ? `${c.approvalState.decided}/${c.approvalState.total}`
                      : <span className="dim">—</span>}</td>
                  <td className="small">{c.plannedStart
                    ? fmtWindow(c.plannedStart, c.plannedEnd)
                    : <span className="dim">Not scheduled</span>}</td>
                  <td className="small">{owner
                    ? <span className="row" style={{gap:6}}><PartyAvatar resolved={owner} size="sm"/>{owner.name}</span>
                    : <span className="dim">—</span>}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function ChangeBoard({changes, idx, statuses, onOpen, onContextMenu, awaiting}){
  return (
    <div className="board">
      {statuses.map(status => {
        const items = changes.filter(c => c.status === status);
        return (
          <div key={status} className="board-col">
            <div className="board-col-head">
              <Badge tone={CHANGE_STATUS_TONE[status] || 'slate'} dot>{status}</Badge>
              <div className="spacer"/><span className="xsmall dim">{items.length}</span>
            </div>
            <div className="board-list">
              {items.map(c => (
                <div className="board-card" key={c.id} onClick={()=>onOpen(c)}
                     onContextMenu={(e)=>onContextMenu && onContextMenu(e, c)} style={{cursor:'pointer'}}>
                  <div className="row" style={{gap:6}}>
                    <ChangeType value={c.changeType}/>
                    <Risk value={c.risk}/>
                    <div className="spacer"/>
                    {awaiting.includes(c.id) && <span style={{color:'var(--red)'}}><I.stamp size={13}/></span>}
                  </div>
                  <div className="ttl">{c.title}</div>
                  <div className="row" style={{gap:6}}>
                    <span className="mono dim" style={{fontSize:11}}>{c.reference}</span>
                    <div className="spacer"/>
                    {c.plannedStart &&
                      <span className="xsmall dim">{fmtDate(c.plannedStart)}</span>}
                  </div>
                </div>
              ))}
              {!items.length &&
                <div className="xsmall dim" style={{padding:'12px 4px',textAlign:'center'}}>Nothing here</div>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function ChangeCalendar({changes, idx, onOpen, onContextMenu, freezeWindows}){
  const [offset, setOffset] = useState(0);
  const base = new Date();
  base.setDate(1);
  base.setMonth(base.getMonth() + offset);
  const year = base.getFullYear(), month = base.getMonth();
  const startDow = (new Date(year, month, 1).getDay() + 6) % 7;
  const gridStart = new Date(year, month, 1 - startDow);
  const cells = Array.from({length: 42}, (_, i) => {
    const d = new Date(gridStart);
    d.setDate(gridStart.getDate() + i);
    return d;
  });
  const iso = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  const today = todayIso();

  // A change occupies every day of its planned window, not only the start.
  const byDate = {};
  changes.forEach(c => {
    if (!c.plannedStart) return;
    const start = new Date(c.plannedStart);
    const end = c.plannedEnd ? new Date(c.plannedEnd) : start;
    for (let d = new Date(start.getFullYear(), start.getMonth(), start.getDate());
         d <= end; d.setDate(d.getDate() + 1)) {
      (byDate[iso(d)] = byDate[iso(d)] || []).push(c);
    }
  });
  const freezeOn = (key) => (freezeWindows || []).filter(f =>
    f.isActive && key >= f.startsAt.slice(0,10) && key <= f.endsAt.slice(0,10));

  const tone = (c) =>
    c.status === 'Rejected' || c.status === 'Failed'
      ? {background:'var(--red-soft)', color:'var(--red)', borderColor:'var(--red-border)'}
    : c.changeType === 'Emergency'
      ? {background:'var(--red-soft)', color:'var(--red)', borderColor:'var(--red-border)'}
    : c.status === 'Closed' || c.status === 'Implemented'
      ? {background:'var(--green-soft)', color:'var(--green)', borderColor:'var(--green-border)'}
    : c.risk === 'High'
      ? {background:'var(--amber-soft)', color:'var(--amber)', borderColor:'var(--amber-border)'}
    : {background:'var(--accent-soft)', color:'var(--accent)', borderColor:'var(--accent-border)'};

  return (
    <div className="card">
      <div className="card-head">
        <button className="icon-btn" onClick={()=>setOffset(o=>o-1)} title="Previous month">
          <I.arrowL size={16}/></button>
        <h3>{base.toLocaleDateString('en-GB',{month:'long',year:'numeric'})}</h3>
        <button className="icon-btn" onClick={()=>setOffset(o=>o+1)} title="Next month">
          <I.arrowR size={16}/></button>
        {offset !== 0 && <button className="btn sm ghost" onClick={()=>setOffset(0)}>Today</button>}
        <div className="spacer"/>
        <span className="xsmall dim">Hatched days fall inside a change freeze</span>
      </div>
      <div className="card-body">
        <div className="cal">
          {['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].map(d => <div className="cal-dow" key={d}>{d}</div>)}
          {cells.map((d, i) => {
            const key = iso(d);
            const items = byDate[key] || [];
            const freezes = freezeOn(key);
            return (
              <div key={i} className={'cal-cell' + (d.getMonth() !== month ? ' other' : '')
                     + (key === today ? ' today' : '') + (freezes.length ? ' freeze-band' : '')}>
                <div className="cal-date">{d.getDate()}</div>
                {freezes.slice(0,1).map(f =>
                  <div className="cal-freeze" key={f.id} title={f.description}>
                    {f.severity === 'Blocking' ? '⛔' : '⚠'} {f.name}</div>)}
                {items.slice(0, 3).map(c =>
                  <div key={c.id} className="cal-item" style={tone(c)} onClick={()=>onOpen(c)}
                       onContextMenu={(e)=>onContextMenu && onContextMenu(e, c)}
                       title={`${c.reference} ${c.title}\n${fmtWindow(c.plannedStart, c.plannedEnd)}`}>
                    {c.reference} {c.title}</div>)}
                {items.length > 3 && <div className="xsmall dim">+{items.length - 3} more</div>}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

/* ----------------------------------------------------------------- detail */

/** Full record page for a change request, mirroring ComponentDetail's layout. */
function ChangeDetail({id, state, idx, go, act, toast, user, canEdit}){
  const [comment, setComment] = useState('');
  const [commentKey, setCommentKey] = useState(0);
  const [busy, setBusy] = useState(false);
  const [deciding, setDeciding] = useState(null);
  const [moving, setMoving] = useState(null);
  const [addingApprover, setAddingApprover] = useState(false);
  const [checks, setChecks] = useState(null);
  const [editing, setEditing] = useState(null);
  const c = state.changes.find(x => x.id === id);

  useEffect(()=>{ setComment(''); }, [id]);

  useEffect(()=>{
    if (!c || !c.plannedStart) { setChecks(null); return; }
    API.get(`/api/changes/${c.id}/schedule-checks`).then(setChecks).catch(()=>setChecks(null));
  }, [c && c.id, c && c.plannedStart, c && c.plannedEnd]);

  if (!c) return <EmptyState icon={<I.warn size={30}/>} title="Change not found"
    body="It may have been deleted by someone else."
    action={<button className="btn" onClick={()=>go({view:'changes'})}>Back to the list</button>}/>;

  const editable = canEdit(c.id);
  const base = `/api/changes/${c.id}`;
  const owner = resolveParty(c.owner, idx);
  const implementer = resolveParty(c.implementer, idx);
  const requester = idx.person[c.requesterPersonId];
  const myApprovals = c.approvals.filter(a => (c.myPendingApprovals || []).includes(a.id));

  const stages = {};
  c.approvals.forEach(a => { (stages[a.stageOrder] = stages[a.stageOrder] || []).push(a); });
  const stageOrders = Object.keys(stages).map(Number).sort((a,b)=>a-b);

  const postComment = async () => {
    if (rteIsBlank(comment)) return;
    setBusy(true);
    const ok = await act(API.post(`${base}/comments`, {body: comment}), 'Comment added');
    if (ok) { setComment(''); setCommentKey(k => k + 1); }
    setBusy(false);
  };

  return (
    <div>
      <div className="page-head">
        <div className="type-icon"><I.change size={20}/></div>
        <div style={{minWidth:0,flex:1}}>
          <div className="row wrap" style={{gap:9,marginBottom:2}}>
            <h1>{c.title}</h1>
            <ChangeType value={c.changeType}/>
            <Risk value={c.risk}/>
            <Badge tone={CHANGE_STATUS_TONE[c.status] || 'slate'} dot>{c.status}</Badge>
            {c.outageRequired && <Badge tone="amber">Outage {c.outageMinutes} min</Badge>}
            {!editable && <Badge tone="slate">Read only</Badge>}
          </div>
          <div className="small muted">
            <span className="mono">{c.reference}</span> · {c.impact} impact
            {c.updatedAt && <> · updated {relative(c.updatedAt)} by {c.updatedBy}</>}
          </div>
        </div>
        {editable && <button className="btn" onClick={()=>setEditing(c)}><I.edit size={14}/>Edit</button>}
      </div>

      <div className="detail">
        <div>
          <ChangeStepper change={c} state={state}/>

          {myApprovals.length > 0 &&
            <div className="info-box row" style={{gap:10, marginTop:14}}>
              <I.stamp size={16}/>
              <span style={{flex:1}}>This change is waiting for your approval at the
                {' '}<strong>{myApprovals[0].stageName}</strong> stage.</span>
              <button className="btn sm primary" onClick={()=>setDeciding(myApprovals[0])}>Decide</button>
            </div>}

          {checks && (checks.blocking.length > 0 || checks.warnings.length > 0) &&
            <div className={checks.blocking.length ? 'err' : 'warn-box'} style={{marginTop:14}}>
              <strong>{checks.blocking.length ? 'Change freeze' : 'Worth knowing'}</strong>
              <ul style={{margin:'6px 0 0 18px', padding:0}}>
                {[...checks.blocking, ...checks.warnings].map((w, i) => <li key={i}>{w.message}</li>)}
              </ul>
            </div>}

          {c.freezeOverride &&
            <div className="warn-box" style={{marginTop:14}}>
              <strong>Freeze overridden.</strong> {c.freezeOverride}
            </div>}

          {editable && c.availableTransitions.length > 0 &&
            <div className="row wrap" style={{gap:6, marginTop:16}}>
              <span className="xsmall dim" style={{marginRight:4}}>Move to</span>
              {c.availableTransitions.map(to =>
                <button key={to} className="chip-toggle" disabled={busy}
                  onClick={()=>setMoving(to)}>{to}</button>)}
            </div>}

          <div className="sub-head">Why</div>
          <p className="pre-wrap muted" style={{marginTop:0}}>{c.reason || 'No reason recorded.'}</p>
          <div className="sub-head">What</div>
          {c.description
            ? <RichText value={c.description}/>
            : <p className="dim" style={{marginTop:0}}>No description recorded.</p>}

          {(c.implementationPlan || c.backoutPlan || c.testPlan) &&
            <>
              <div className="sub-head">Plans</div>
              <div className="stack" style={{gap:10}}>
                {[['Implementation', c.implementationPlan], ['Backout', c.backoutPlan],
                  ['Test', c.testPlan], ['Communications', c.communicationsPlan]]
                  .filter(([, text]) => text).map(([label, text]) => (
                    <div className="card" key={label}>
                      <div className="card-head"><h3>{label}</h3></div>
                      <div className="card-body pre-wrap muted" style={{fontSize:13.5}}>{text}</div>
                    </div>))}
              </div>
            </>}

          {/* ---------------------------------------------------- approvals */}
          <div className="sub-head">Approvals</div>
          {stageOrders.length === 0
            ? <p className="dim small" style={{marginTop:0}}>
                {c.changeType === 'Standard'
                  ? 'Standard changes are pre-approved, so there is nothing to sign off.'
                  : 'Approvals are built from a template when the change is submitted.'}</p>
            : stageOrders.map(order => {
                const rows = stages[order];
                const satisfied = c.approvalState.stages.find(s => s.order === order);
                const isCurrent = c.approvalState.currentStage === order;
                return (
                  <div className={'approval-stage' + (isCurrent ? ' current' : '')} key={order}>
                    <div className="approval-stage-head">
                      <strong style={{fontSize:13}}>{rows[0].stageName}</strong>
                      <Badge tone="slate">{rows[0].rule === 'all' ? 'Everyone must approve' : 'Any one approver'}</Badge>
                      {satisfied && satisfied.satisfied && <Badge tone="green" dot>Complete</Badge>}
                      {satisfied && satisfied.rejected && <Badge tone="red" dot>Rejected</Badge>}
                      {isCurrent && <Badge tone="accent" dot>Current stage</Badge>}
                    </div>
                    {rows.map(a => {
                      const party = resolveParty(a.party, idx);
                      const mineToDecide = (c.myPendingApprovals || []).includes(a.id);
                      return (
                        <div className="approver-row" key={a.id}>
                          <PartyAvatar resolved={party} size="sm"/>
                          <div style={{flex:1,minWidth:0}}>
                            <div style={{fontWeight:600,fontSize:13}}>
                              {party ? party.name : 'Unknown'}
                              {a.isAdhoc && <Badge tone="slate" >Added for this change</Badge>}
                            </div>
                            {a.comment && <div className="xsmall muted pre-wrap" style={{marginTop:2}}>{a.comment}</div>}
                            {a.decidedAt &&
                              <div className="xsmall dim">{a.decision} by {a.decidedBy} · {relative(a.decidedAt)}</div>}
                          </div>
                          <span className={'decision ' + a.decision}>{a.decision}</span>
                          {mineToDecide &&
                            <button className="btn sm primary" onClick={()=>setDeciding(a)}>Decide</button>}
                          {editable && a.decision === 'Pending' && !mineToDecide &&
                            <button className="icon-btn" title="Remove this approver"
                              onClick={()=>act(API.del(`${base}/approvals/${a.id}`), 'Approver removed')}>
                              <I.x size={14}/></button>}
                        </div>
                      );
                    })}
                  </div>
                );
              })}
          {editable && !['Closed','Cancelled','Rejected'].includes(c.status) &&
            <button className="btn sm" onClick={()=>setAddingApprover(true)}>
              <I.plus size={13}/>Add an approver</button>}

          {/* --------------------------------------------------- linkage */}
          <div className="sub-head">Business requests</div>
          {(c.requestIds||[]).length
            ? <div className="pill-list">
                {c.requestIds.map(rid => idx.request[rid]
                  ? <span key={rid} className="chip-toggle" style={{cursor:'pointer'}}
                      onClick={()=>go({view:'request', id:rid})}>
                      {idx.request[rid].reference} {idx.request[rid].title}</span>
                  : null)}
              </div>
            : <p className="dim small" style={{margin:0}}>Not raised from a business request.</p>}

          <div className="sub-head">Components affected</div>
          {(c.componentIds||[]).length
            ? <div className="pill-list">
                {c.componentIds.map(cid => idx.comp[cid]
                  ? <span key={cid} className="chip-toggle" style={{cursor:'pointer'}}
                      onClick={()=>go({view:'component', id:cid})}>
                      <TierBadge tier={idx.comp[cid].tier}/>
                      <span style={{marginLeft:5}}>{idx.comp[cid].name}</span></span>
                  : null)}
              </div>
            : <p className="dim small" style={{margin:0}}>None linked.</p>}

          {/* --------------------------------------------------- comments */}
          <div className="sub-head">Discussion</div>
          {c.comments.length === 0 && <p className="dim small">No comments yet.</p>}
          {c.comments.map(x => (
            <div className="comment" key={x.id}>
              <Avatar name={x.createdBy} size="sm"/>
              <div style={{flex:1,minWidth:0}}>
                <div className="row" style={{gap:7}}>
                  <strong style={{fontSize:13}}>{x.createdBy}</strong>
                  <span className="xsmall dim">{fmtDateTime(x.createdAt)}</span>
                </div>
                <RichText value={x.body} className="comment-body"/>
              </div>
            </div>
          ))}
          <div style={{marginTop:12}}>
            <RichTextEditor key={commentKey} mode="compact" value={comment} onChange={setComment} toast={toast}
              uploadContext={{entityKind:'change', entityId:c.id}} placeholder="Add a comment…"/>
            <div className="row" style={{marginTop:8}}>
              <div className="spacer"/>
              <button className="btn primary sm" disabled={rteIsBlank(comment) || busy} onClick={postComment}>
                <I.comment size={13}/>Comment</button>
            </div>
          </div>

          {c.events.length > 0 &&
            <><div className="sub-head">History</div>
              <div className="side-list">
                {c.events.map(e =>
                  <div className="side-row" key={e.id}>
                    <span className="k">{fmtDateTime(e.at)}</span>
                    <span className="v small">
                      {e.by} changed {e.field}{e.oldValue ? ` from ${e.oldValue}` : ''} to{' '}
                      <strong>{e.newValue || 'nothing'}</strong>
                      {e.note && <span className="dim"> — {e.note}</span>}
                    </span>
                  </div>)}
              </div></>}

          <div style={{marginTop:18}}>
            <AttachmentPanel attachments={c.attachments} base={base} editable={editable}
              act={act} toast={toast} title="Evidence and attachments"/>
          </div>
        </div>

        <div className="stack">
          <div className="card">
            <div className="card-head"><h3>Change details</h3></div>
            <div className="side-list">
              <div className="side-row"><span className="k">Planned window</span>
                <span className="v">{c.plannedStart ? fmtWindow(c.plannedStart, c.plannedEnd) : '—'}</span></div>
              {c.actualStart &&
                <div className="side-row"><span className="k">Actual</span>
                  <span className="v">{fmtWindow(c.actualStart, c.actualEnd)}</span></div>}
              <div className="side-row"><span className="k">Change owner</span>
                <span className="v">{owner
                  ? <span className="row" style={{gap:7,justifyContent:'flex-end'}}>
                      <PartyAvatar resolved={owner} size="sm"/>{owner.name}</span>
                  : <span className="dim">Not assigned</span>}</span></div>
              <div className="side-row"><span className="k">Implementer</span>
                <span className="v">{implementer
                  ? <span className="row" style={{gap:7,justifyContent:'flex-end'}}>
                      <PartyAvatar resolved={implementer} size="sm"/>{implementer.name}</span>
                  : <span className="dim">Not assigned</span>}</span></div>
              <div className="side-row"><span className="k">Requested by</span>
                <span className="v">{requester ? requester.name : c.raisedBy}</span></div>
              {c.submittedAt &&
                <div className="side-row"><span className="k">Submitted</span><span className="v">{fmtDateTime(c.submittedAt)}</span></div>}
              {c.approvedAt &&
                <div className="side-row"><span className="k">Approved</span><span className="v">{fmtDateTime(c.approvedAt)}</span></div>}
              {c.reviewOutcome &&
                <div className="side-row"><span className="k">Review outcome</span><span className="v">
                  <Badge tone={c.reviewOutcome === 'Successful' ? 'green' : c.reviewOutcome === 'Failed' ? 'red' : 'amber'}>
                    {c.reviewOutcome}</Badge></span></div>}
            </div>
          </div>
        </div>
      </div>

      {deciding &&
        <DecisionModal approval={deciding} change={c} idx={idx} onClose={()=>setDeciding(null)}
          onDecide={async (decision, text)=>{
            const ok = await act(
              API.post(`${base}/approvals/${deciding.id}/decision`, {decision, comment: text}),
              decision === 'Approved' ? 'Approval recorded' : 'Rejection recorded');
            if (ok) setDeciding(null);
          }}/>}

      {moving &&
        <TransitionModal to={moving} change={c} checks={checks} onClose={()=>setMoving(null)}
          onConfirm={async (note, override)=>{
            const ok = await act(API.post(`${base}/transition`,
              {status: moving, note, freezeOverride: override}), `Moved to ${moving}`);
            if (ok) setMoving(null);
          }}/>}

      {addingApprover &&
        <AddApproverModal change={c} state={state} onClose={()=>setAddingApprover(false)}
          onSave={async (payload)=>{
            const ok = await act(API.post(`${base}/approvals`, payload), 'Approver added');
            if (ok) setAddingApprover(false);
          }}/>}

      {editing &&
        <ChangeForm value={editing} state={state} onClose={()=>setEditing(null)} toast={toast}
          onSave={async (v)=>{
            const ok = await act(API.put(`/api/changes/${v.id}`, v), 'Change saved');
            if (ok) setEditing(null);
          }}/>}
    </div>
  );
}

/* ------------------------------------------------------------- modals */

function DecisionModal({approval, change, idx, onClose, onDecide}){
  const [comment, setComment] = useState('');
  const [busy, setBusy] = useState(false);
  const decide = async (decision) => {
    if (decision === 'Rejected' && !comment.trim()) return;
    setBusy(true);
    try { await onDecide(decision, comment); } finally { setBusy(false); }
  };
  return (
    <Modal title={`${approval.stageName}: ${change.reference}`} onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn danger" disabled={busy || !comment.trim()}
          onClick={()=>decide('Rejected')}>Reject</button>
        <button className="btn primary" disabled={busy} onClick={()=>decide('Approved')}>Approve</button>
      </>}>
      <div className="info-box" style={{marginBottom:16}}>
        <strong>{change.title}</strong><br/>
        <span className="small">{change.changeType} change, {change.risk.toLowerCase()} risk,
          planned for {fmtWindow(change.plannedStart, change.plannedEnd).toLowerCase()}.</span>
      </div>
      {change.backoutPlan &&
        <Field label="Backout plan">
          <div className="pre-wrap small muted" style={{padding:'10px 12px',border:'1px solid var(--border)',
            borderRadius:'var(--radius-sm)',background:'var(--surface-2)'}}>{change.backoutPlan}</div>
        </Field>}
      <Field label="Comment" hint="Required if you are rejecting, optional if you are approving.">
        <textarea value={comment} onChange={e=>setComment(e.target.value)} autoFocus
          placeholder="Anything the team should know about your decision"/>
      </Field>
    </Modal>
  );
}

function TransitionModal({to, change, checks, onClose, onConfirm}){
  const [note, setNote] = useState('');
  const [override, setOverride] = useState('');
  const [busy, setBusy] = useState(false);
  const blocking = (checks && checks.blocking) || [];
  const needsOverride = blocking.length > 0 && (to === 'Scheduled' || to === 'In progress');
  return (
    <Modal title={`Move ${change.reference} to ${to}`} onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn primary" disabled={busy || (needsOverride && !override.trim())}
          onClick={async ()=>{ setBusy(true); try { await onConfirm(note, override); } finally { setBusy(false); } }}>
          {busy ? 'Working…' : `Move to ${to}`}</button>
      </>}>
      {to === 'Awaiting approval' &&
        <div className="info-box" style={{marginBottom:16}}>
          Submitting builds the approval stages from the template that matches a
          {' '}{change.changeType.toLowerCase()} change at {change.risk.toLowerCase()} risk, and
          notifies the first set of approvers.
        </div>}
      {needsOverride &&
        <div className="err">
          <strong>This window falls inside a change freeze.</strong>
          <ul style={{margin:'6px 0 0 18px',padding:0}}>
            {blocking.map((b,i) => <li key={i}>{b.message}</li>)}
          </ul>
        </div>}
      {needsOverride &&
        <Field label="Override reason" hint="Recorded against the change and written to the audit log.">
          <textarea value={override} onChange={e=>setOverride(e.target.value)} autoFocus/>
        </Field>}
      <Field label="Note" hint="Optional. Shown in the history and in the notification.">
        <textarea value={note} onChange={e=>setNote(e.target.value)}/>
      </Field>
    </Modal>
  );
}

function AddApproverModal({change, state, onClose, onSave}){
  const [party, setParty] = useState(null);
  const [stageOrder, setStageOrder] = useState(
    change.approvalState.currentStage != null ? String(change.approvalState.currentStage) : '0');
  const [stageName, setStageName] = useState('');
  const stages = change.approvalState.stages;
  return (
    <Modal title="Add an approver" onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn primary" disabled={!party}
          onClick={()=>onSave({party, stageOrder: Number(stageOrder), stageName})}>Add</button>
      </>}>
      <p className="small muted" style={{marginTop:0}}>
        Adds one more approver to this change only. The template is not affected.
      </p>
      <Field label="Person or role">
        <PartySelect value={party} state={state} onChange={setParty} placeholder="Choose…"/>
      </Field>
      <Field label="Stage">
        <select value={stageOrder} onChange={e=>setStageOrder(e.target.value)}>
          {stages.map(s => <option key={s.order} value={s.order}>{s.order + 1}. {s.name}</option>)}
          {!stages.length && <option value="0">First stage</option>}
        </select>
      </Field>
      {!stages.length &&
        <Field label="Stage name"><input type="text" value={stageName}
          onChange={e=>setStageName(e.target.value)} placeholder="e.g. Additional approval"/></Field>}
    </Modal>
  );
}

/* ------------------------------------------------------------------- form */

function ChangeForm({value, state, onClose, onSave, toast}){
  const [c, setC] = useState(()=>clone(value));
  const [busy, setBusy] = useState(false);
  const set = (k,v) => setC(x => ({...x, [k]:v}));
  const isNew = !value.id;
  const fromRequest = value.fromRequestId ? state.requests.find(r => r.id === value.fromRequestId) : null;

  // Show which template the change will pick up, before it is submitted.
  const template = useMemo(()=>{
    const score = (t) =>
      (t.changeType === c.changeType ? 4 : t.changeType === '' ? 1 : -100) +
      (t.risk === c.risk ? 2 : t.risk === '' ? 1 : -100) + (t.isDefault ? 0.5 : 0);
    const viable = (state.approvalTemplates||[]).filter(t => t.isActive)
      .map(t => ({t, s: score(t)})).filter(x => x.s > 0).sort((a,b)=>b.s-a.s);
    return viable.length ? viable[0].t : null;
  }, [c.changeType, c.risk, state.approvalTemplates]);

  return (
    <Modal wide title={isNew ? 'Raise a change' : `Edit ${value.reference}`} onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn primary" disabled={!c.title.trim() || busy}
          onClick={async ()=>{ setBusy(true); try { await onSave(c); } finally { setBusy(false); } }}>
          {busy ? 'Saving…' : isNew ? 'Raise change' : 'Save changes'}</button>
      </>}>
      {fromRequest &&
        <div className="info-box" style={{marginBottom:16}}>
          Raised from <strong>{fromRequest.reference} {fromRequest.title}</strong>. The description,
          requester and affected components have been carried across, and the two records will stay
          linked.
        </div>}

      <Field label="Title"><input type="text" value={c.title} autoFocus
        onChange={e=>set('title', e.target.value)} placeholder="What is being changed"/></Field>
      <Field label="Reason" hint="Why this is being done. The approvers read this first.">
        <textarea value={c.reason} onChange={e=>set('reason', e.target.value)}/></Field>
      <Field label="Description">
        <RichTextEditor value={c.description} onChange={v=>set('description', v)} toast={toast}
          uploadContext={c.id ? {entityKind:'change', entityId:c.id} : null}/>
      </Field>

      <div className="sub-head">Classification</div>
      <div className="form-grid three">
        <Field label="Type" hint={c.changeType === 'Standard' ? 'Pre-approved, no approval stages'
          : c.changeType === 'Emergency' ? 'May proceed before approval completes' : ''}>
          <select value={c.changeType} onChange={e=>set('changeType', e.target.value)}>
            {(state.changeTypes||[]).map(t => <option key={t}>{t}</option>)}</select></Field>
        <Field label="Risk">
          <select value={c.risk} onChange={e=>set('risk', e.target.value)}>
            {(state.changeRisks||[]).map(t => <option key={t}>{t}</option>)}</select></Field>
        <Field label="Impact">
          <select value={c.impact} onChange={e=>set('impact', e.target.value)}>
            {(state.changeRisks||[]).map(t => <option key={t}>{t}</option>)}</select></Field>
      </div>
      <div className={template ? 'info-box' : 'warn-box'} style={{marginBottom:16}}>
        {c.changeType === 'Standard'
          ? 'Standard changes are pre-approved and skip the approval stages entirely.'
          : template
            ? <>Approval route: <strong>{template.name}</strong> — {' '}
                {template.stages.map(s => s.name).join(' → ')}</>
            : 'No approval template matches this combination yet. An administrator can add one, ' +
              'or you can add approvers by hand once the change is raised.'}
      </div>

      <div className="sub-head">Who</div>
      <div className="form-grid three">
        <Field label="Change owner" hint="Accountable for the change">
          <PartySelect value={c.owner} state={state} onChange={v=>set('owner', v)}/></Field>
        <Field label="Implementer" hint="Doing the work">
          <PartySelect value={c.implementer} state={state} onChange={v=>set('implementer', v)}/></Field>
        <Field label="Requested by">
          <select value={c.requesterPersonId || ''} onChange={e=>set('requesterPersonId', e.target.value)}>
            <option value="">Not recorded</option>
            {state.people.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </select></Field>
      </div>

      <div className="sub-head">When</div>
      <div className="form-grid">
        <Field label="Planned start">
          <input type="datetime-local" value={toLocalInput(c.plannedStart)}
            onChange={e=>set('plannedStart', fromLocalInput(e.target.value))}/></Field>
        <Field label="Planned end">
          <input type="datetime-local" value={toLocalInput(c.plannedEnd)}
            onChange={e=>set('plannedEnd', fromLocalInput(e.target.value))}/></Field>
      </div>
      <label className="row" style={{gap:8,cursor:'pointer',marginBottom:14}}>
        <input type="checkbox" checked={c.outageRequired}
          onChange={e=>set('outageRequired', e.target.checked)}/>
        <span className="small">An outage is required</span>
      </label>
      {c.outageRequired &&
        <Field label="Expected outage" hint="Minutes">
          <input type="number" min="0" value={c.outageMinutes}
            onChange={e=>set('outageMinutes', e.target.value)}/></Field>}

      <div className="sub-head">Plans</div>
      <Field label="Implementation plan" hint="The steps, in order.">
        <textarea style={{minHeight:110}} value={c.implementationPlan}
          onChange={e=>set('implementationPlan', e.target.value)}/></Field>
      <Field label="Backout plan" hint="How this is undone if it goes wrong. Approvers look for this.">
        <textarea value={c.backoutPlan} onChange={e=>set('backoutPlan', e.target.value)}/></Field>
      <Field label="Test plan"><textarea value={c.testPlan}
        onChange={e=>set('testPlan', e.target.value)}/></Field>
      <Field label="Communications plan" hint="Who is told, and when.">
        <textarea value={c.communicationsPlan}
          onChange={e=>set('communicationsPlan', e.target.value)}/></Field>

      {!isNew && ['Implemented','Under review','Failed','Closed'].includes(value.status) &&
        <>
          <div className="sub-head">Post-implementation review</div>
          <div className="form-grid">
            <Field label="Outcome">
              <select value={c.reviewOutcome} onChange={e=>set('reviewOutcome', e.target.value)}>
                <option value="">Not yet reviewed</option>
                {(state.lookups.change_review_outcome||[]).map(o => <option key={o}>{o}</option>)}
              </select></Field>
            <Field label="Actual start">
              <input type="datetime-local" value={toLocalInput(c.actualStart)}
                onChange={e=>set('actualStart', fromLocalInput(e.target.value))}/></Field>
          </div>
          <Field label="Review notes"><textarea value={c.reviewNotes}
            onChange={e=>set('reviewNotes', e.target.value)}/></Field>
        </>}

      <div className="sub-head">Components affected</div>
      <ComponentPicker state={state} value={c.componentIds} onChange={v=>set('componentIds', v)}/>
    </Modal>
  );
}
