/* ============================================================
   BUSINESS REQUESTS MODULE
   List, board, table, timeline and calendar views over the same data.
   ============================================================ */

const CLOSED_STATUSES = ['Done', 'Rejected'];
const isClosedRequest = (r) => CLOSED_STATUSES.includes(r.status);

/** Compact table, also used on a component's Requests tab (where Th/bulk selection
 * are omitted — sorting and bulk actions only make sense on the module's own list). */
function RequestTable({requests, idx, go, onOpen, onContextMenu, Th, selected, onToggle, onToggleAll}){
  const Head = Th || ((props) => {
    const rest = Object.assign({}, props);
    delete rest.label;
    return <th {...rest}>{props.label}</th>;
  });
  const bulk = !!selected;
  return (
    <div className="tbl-wrap">
      <table>
        <thead><tr>
          {bulk && <th style={{width:34}}>
            <input type="checkbox" aria-label="Select all shown"
              checked={requests.length > 0 && requests.every(r=>selected.has(r.id))}
              onChange={()=>onToggleAll(requests.map(r=>r.id))}/></th>}
          <Head k="reference" label="Ref" style={{width:90}}/>
          <Head k="title" label="Title"/>
          <Head k="type" label="Type"/>
          <Head k="priority" label="Priority"/>
          <Head k="status" label="Status"/>
          <Head k="assignee" label="Assignee"/>
          <Head k="target" label="Target"/>
        </tr></thead>
        <tbody>
          {requests.map(r => {
            const assignee = resolveParty(r.assignee, idx);
            const overdue = isOverdue(r.targetDate) && !isClosedRequest(r);
            return (
              <tr key={r.id} className={onOpen ? 'clickable' : ''} onClick={()=>onOpen && onOpen(r)}
                  onContextMenu={(e)=>onContextMenu && onContextMenu(e, r)}>
                {bulk && <td onClick={e=>e.stopPropagation()}>
                  <input type="checkbox" aria-label={`Select ${r.reference}`}
                    checked={selected.has(r.id)} onChange={()=>onToggle(r.id)}/></td>}
                <td className="mono dim">{r.reference}</td>
                <td>
                  <div style={{fontWeight:600}}>{r.title}</div>
                  <div className="cell-sub">
                    {(r.componentIds||[]).map(cid => (idx.comp[cid]||{}).identifier).filter(Boolean).join(', ')
                      || 'No components linked'}
                  </div>
                </td>
                <td className="small muted">{r.requestType}</td>
                <td><Priority value={r.priority}/></td>
                <td><Badge tone={REQUEST_STATUS_TONE[r.status] || 'slate'} dot>{r.status}</Badge></td>
                <td className="small">{assignee
                  ? <span className="row" style={{gap:6}}><PartyAvatar resolved={assignee} size="sm"/>{assignee.name}</span>
                  : <span className="dim">Unassigned</span>}</td>
                <td className="small">{r.targetDate
                  ? <span style={overdue ? {color:'var(--red)',fontWeight:600} : null}>{fmtDate(r.targetDate)}</span>
                  : <span className="dim">—</span>}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

const NEW_REQUEST = (personId) => ({
  id:'', title:'', description:'', requestType:'Service request', priority:'P3', status:'New',
  requesterPersonId: personId || '', assignee:null, businessArea:'', targetDate:'', startDate:'',
  effortEstimate:0, resolution:'', componentIds:[]
});

function RequestsModule({state, idx, go, act, toast, user, route}){
  const [view, setView] = useState(()=>urlParam('view','board'));
  const [q, setQ] = useState(()=>urlParam('q'));
  const [fType, setFType] = useState(()=>urlParam('type'));
  const [fPriority, setFPriority] = useState(()=>urlParam('priority'));
  const [fStatus, setFStatus] = useState(()=>urlParam('status'));
  const [fArea, setFArea] = useState(()=>urlParam('area'));
  const [fAssignee, setFAssignee] = useState(()=>urlParam('assignee'));
  const [mine, setMine] = useState(()=>urlParam('mine')==='1');
  const [showClosed, setShowClosed] = useState(()=>urlParam('closed')==='1');
  const [editing, setEditing] = useState(null);
  const onOpen = (r) => go({view:'request', id:r.id});
  const {menu, openMenu, closeMenu} = useRecordMenu();
  const onContextMenu = (e, r) => openMenu(e, recordMenuItem('request', r));
  const bulk = useBulkSelect();
  const [bulkStatusVal, setBulkStatusVal] = useState('');
  const [bulkAssigneeVal, setBulkAssigneeVal] = useState('');
  useEffect(()=>{ bulk.clear(); }, [view]);

  const canEdit = (id) => {
    const ids = state.permissions.editableRequestIds;
    return ids === '*' || (Array.isArray(ids) && ids.includes(id));
  };
  const isMine = useCallback((r) =>
    r.raisedByUserId === user.id
    || (user.personId && r.requesterPersonId === user.personId)
    || (user.personId && r.assignee && r.assignee.kind === 'person' && r.assignee.id === user.personId)
    || (user.personId && r.assignee && r.assignee.kind === 'role'
        && (idx.role[r.assignee.id] || {}).primaryPersonId === user.personId),
    [user, idx]);

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

  useFilterUrlSync({
    view:[view,'board'], q:[q,''], type:[fType,''], priority:[fPriority,''], status:[fStatus,''],
    area:[fArea,''], assignee:[fAssignee,''], mine:[mine?'1':'',''], closed:[showClosed?'1':'','']
  });

  const {sort, sorted, Th} = useSort('target', {
    reference: r => r.reference, title: r => r.title, type: r => r.requestType,
    priority: r => r.priority, status: r => r.status,
    assignee: r => partyName(r.assignee, idx) || 'zzz', target: r => r.targetDate || '9999'
  });

  const clearFilters = () => {
    setQ('');setFType('');setFPriority('');setFStatus('');setFArea('');setFAssignee('');setMine(false);
  };

  const rows = useMemo(()=>{
    const term = q.trim().toLowerCase();
    const list = state.requests.filter(r => {
      if (!showClosed && isClosedRequest(r)) return false;
      if (fType && r.requestType !== fType) return false;
      if (fPriority && r.priority !== fPriority) return false;
      if (fStatus && r.status !== fStatus) return false;
      if (fArea && r.businessArea !== fArea) return false;
      if (fAssignee && partyKey(r.assignee) !== fAssignee) return false;
      if (mine && !isMine(r)) return false;
      if (!term) return true;
      return [r.reference, r.title, r.description, r.requestType, r.businessArea,
        partyName(r.assignee, idx),
        (r.componentIds||[]).map(cid => (idx.comp[cid]||{}).name).join(' ')
      ].join(' ').toLowerCase().includes(term);
    });
    return view === 'table' ? sorted(list) : list;
  }, [state.requests, q, fType, fPriority, fStatus, fArea, fAssignee, mine, showClosed, idx, isMine, view, sort]);

  const bulkApplyStatus = async () => {
    if (!bulkStatusVal) return;
    const ids = [...bulk.selected];
    try {
      for (const id of ids) await API.put(`/api/requests/${id}/status`, {status: bulkStatusVal});
      await act(Promise.resolve(), `Moved ${ids.length} request${ids.length===1?'':'s'} to ${bulkStatusVal}`);
    } catch (err) { toast(err.message, true); }
    finally { bulk.clear(); setBulkStatusVal(''); }
  };
  const bulkApplyAssignee = async () => {
    if (!bulkAssigneeVal) return;
    const party = parsePartyKey(bulkAssigneeVal);
    const ids = [...bulk.selected];
    try {
      for (const id of ids) {
        const r = state.requests.find(x => x.id === id);
        if (r) await API.put(`/api/requests/${id}`, {...r, assignee: party});
      }
      await act(Promise.resolve(), `Assigned ${ids.length} request${ids.length===1?'':'s'}`);
    } catch (err) { toast(err.message, true); }
    finally { bulk.clear(); setBulkAssigneeVal(''); }
  };

  const statuses = state.requestStatuses || [];
  const openCount = state.requests.filter(r => !isClosedRequest(r)).length;
  const overdueCount = state.requests.filter(r => isOverdue(r.targetDate) && !isClosedRequest(r)).length;
  const p1 = state.requests.filter(r => r.priority === 'P1' && !isClosedRequest(r)).length;
  const unassigned = state.requests.filter(r => !r.assignee && !isClosedRequest(r)).length;

  const assigneeOptions = [
    ...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 exportList = () => exportCsv('fuse-requests.csv',
    ['Reference','Title','Type','Priority','Status','Requester','Assignee','Business area',
     'Start','Target','Effort','Components','Raised','Closed'],
    rows.map(r => [r.reference, r.title, r.requestType, r.priority, r.status,
      (idx.person[r.requesterPersonId]||{}).name || '', partyName(r.assignee, idx), r.businessArea,
      r.startDate, r.targetDate, r.effortEstimate,
      (r.componentIds||[]).map(cid => (idx.comp[cid]||{}).identifier).filter(Boolean).join(' '),
      r.createdAt, r.closedAt]));

  const hasFilters = !!(q||fType||fPriority||fStatus||fArea||fAssignee||mine);
  const shared = {requests: rows, state, idx, go, act, user, onOpen, onContextMenu, canEdit, hasFilters, clearFilters};

  return (
    <div className="mod">
      <div className="mod-head">
        <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: p1 ? 'var(--red)' : undefined}}>{p1}</div>
            <div className="l">P1 open</div></div>
          <div className="stat"><div className="n" style={{color: overdueCount ? 'var(--amber)' : undefined}}>{overdueCount}</div>
            <div className="l">Past target</div></div>
          <div className="stat"><div className="n">{unassigned}</div><div className="l">Unassigned</div></div>
          <div className="stat"><div className="n">{state.requests.filter(r=>isMine(r) && !isClosedRequest(r)).length}</div>
            <div className="l">Mine, open</div></div>
        </div>

        <div className="row wrap">
          <ViewSwitch value={view} onChange={setView} views={[
            {k:'board', label:'Board', icon:<I.board size={13}/>},
            {k:'list', label:'List', icon:<I.list size={13}/>},
            {k:'table', label:'Table', icon:<I.table size={13}/>},
            {k:'timeline', label:'Timeline', icon:<I.timeline 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 business requests"
                       title="Search reference, title or component"/>
            <select style={{width:'auto'}} value={fType} onChange={e=>setFType(e.target.value)}>
              <option value="">All types</option>
              {(state.requestTypes||[]).map(t=><option key={t}>{t}</option>)}</select>
            <select style={{width:'auto'}} value={fPriority} onChange={e=>setFPriority(e.target.value)}>
              <option value="">All priorities</option>
              {(state.requestPriorities||[]).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>
                {statuses.map(t=><option key={t}>{t}</option>)}</select>}
            <select style={{width:'auto'}} value={fArea} onChange={e=>setFArea(e.target.value)}>
              <option value="">All areas</option>
              {(state.lookups.business_area||[]).map(t=><option key={t}>{t}</option>)}</select>
            <select style={{width:'auto',maxWidth:200}} value={fAssignee} onChange={e=>setFAssignee(e.target.value)}>
              <option value="">Anyone</option>
              {assigneeOptions.map(o=><option key={o.k} value={o.k}>{o.label}</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>
            {(q||fType||fPriority||fStatus||fArea||fAssignee||mine) &&
              <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 === 'board' && <RequestBoard {...shared} statuses={statuses}/>}
        {view === 'list' && <div className="stack mod-scroll"><RequestList {...shared}/></div>}
        {view === 'table' &&
          <div className="card">
            {rows.length > 0 &&
              <BulkBar count={bulk.selected.size} onClear={bulk.clear}>
                <select style={{width:'auto'}} value={bulkStatusVal} onChange={e=>setBulkStatusVal(e.target.value)}>
                  <option value="">Move to…</option>
                  {statuses.map(s=><option key={s} value={s}>{s}</option>)}
                </select>
                <button className="btn sm" disabled={!bulkStatusVal} onClick={bulkApplyStatus}>Apply</button>
                <select style={{width:'auto',maxWidth:200}} value={bulkAssigneeVal}
                        onChange={e=>setBulkAssigneeVal(e.target.value)}>
                  <option value="">Assign to…</option>
                  {assigneeOptions.map(o=><option key={o.k} value={o.k}>{o.label}</option>)}
                </select>
                <button className="btn sm" disabled={!bulkAssigneeVal} onClick={bulkApplyAssignee}>Apply</button>
              </BulkBar>}
            {rows.length
              ? <RequestTable requests={rows} idx={idx} go={go} onOpen={onOpen} onContextMenu={onContextMenu} Th={Th}
                  selected={bulk.selected} onToggle={bulk.toggle} onToggleAll={bulk.toggleAll}/>
              : <EmptyState icon={<I.inbox size={28}/>} title="No requests match"
                  action={(q||fType||fPriority||fStatus||fArea||fAssignee||mine)
                    ? <button className="btn sm" onClick={clearFilters}>Clear filters</button> : null}/>}
          </div>}
        {view === 'timeline' && <div className="mod-scroll"><RequestTimeline {...shared}/></div>}
        {view === 'calendar' && <div className="mod-scroll"><RequestCalendar {...shared}/></div>}
        {view === 'analytics' && <div className="mod-scroll"><RequestsAnalytics state={state} idx={idx} go={go}/></div>}
      </div>

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

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

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

function RequestsAnalytics({state, idx, go}){
  const requests = state.requests;
  const open = requests.filter(r => !isClosedRequest(r));

  const byStatus = (state.requestStatuses||[]).map(s => ({k:s, n: requests.filter(r=>r.status===s).length}));
  const byPriority = (state.requestPriorities||[]).map(p => ({k:p, n: open.filter(r=>r.priority===p).length}));
  const byType = (state.requestTypes||[]).map(t => ({k:t, n: open.filter(r=>r.requestType===t).length}))
    .sort((a,b)=>b.n-a.n);
  const byArea = {};
  open.forEach(r => { const k = r.businessArea || 'Unspecified'; byArea[k] = (byArea[k]||0)+1; });
  const areaRows = Object.entries(byArea).sort((a,b)=>b[1]-a[1]);

  const overdue = open.filter(r => isOverdue(r.targetDate));
  const onTrack = open.length - overdue.length;

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

  // Requests raised per month, oldest to newest, for the last 6 months.
  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 raisedTrend = monthKeys.map(m => ({...m, n: requests.filter(r => (r.createdAt||'').slice(0,7) === m.ym).length}));
  const maxTrend = Math.max(1, ...raisedTrend.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">{requests.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={requests.length}/>
                </span>
                <strong className="small">{n}</strong>
              </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Open requests by priority</h3><Badge tone="slate">{open.length}</Badge></div>
          <div className="card-body tight">
            {byPriority.map(({k,n}) =>
              <div className="side-row" key={k}>
                <span style={{minWidth:0,flex:1}}>
                  <span style={{fontWeight:600}}><Priority value={k}/></span>
                  <Bar n={n} max={open.length}
                    tone={k==='P1' ? 'var(--red)' : k==='P2' ? 'var(--amber)' : k==='P3' ? 'var(--blue)' : 'var(--slate)'}/>
                </span>
                <strong className="small">{n}</strong>
              </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Open requests by type</h3></div>
          <div className="card-body tight">
            {byType.length === 0
              ? <EmptyState icon={<I.inbox 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}}>{k}</span>
                      <Bar n={n} max={byType[0].n}/>
                    </span>
                    <strong className="small">{n}</strong>
                  </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Target date performance</h3>
            <Badge tone={overdue.length ? 'amber' : 'green'}>{overdue.length} past target</Badge></div>
          <div className="card-body tight">
            <div className="side-row">
              <span style={{fontWeight:600}}>On track</span>
              <strong className="small">{onTrack}</strong>
            </div>
            <div className="side-row">
              <span style={{fontWeight:600}}>Past target</span>
              <strong className="small">{overdue.length}</strong>
            </div>
            {overdue.length > 0 &&
              <div className="card-body" style={{paddingTop:2}}>
                {overdue.slice(0,6).map(r =>
                  <div key={r.id} className="xsmall dim clickable" style={{padding:'4px 0'}}
                       onClick={()=>go({view:'request', id:r.id})}>
                    <span className="mono">{r.reference}</span> {r.title} — target {fmtDate(r.targetDate)}
                  </div>)}
                {overdue.length > 6 && <div className="xsmall dim">+{overdue.length-6} more</div>}
              </div>}
          </div>
        </div>

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

        <div className="card">
          <div className="card-head"><h3>Busiest assignees</h3></div>
          <div className="card-body tight">
            {assigneeRows.length === 0
              ? <EmptyState icon={<I.users size={26}/>} title="Nothing open"/>
              : assigneeRows.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={assigneeRows[0][1]}/>
                    </span>
                    <strong className="small">{n}</strong>
                  </div>)}
          </div>
        </div>
      </div>

      <div className="card">
        <div className="card-head"><h3>Raised per month</h3>
          <span className="xsmall dim">Last 6 months, all types and statuses</span></div>
        <div className="card-body tight">
          {raisedTrend.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>
  );
}

/* ------------------------------------------------------------------ board */

function RequestCard({request, idx, onOpen, onContextMenu, draggable, onDragStart, dragging}){
  const assignee = resolveParty(request.assignee, idx);
  const overdue = isOverdue(request.targetDate) && !isClosedRequest(request);
  return (
    <div className={'board-card' + (dragging ? ' dragging' : '')}
         draggable={draggable} onDragStart={onDragStart} onClick={()=>onOpen(request)}
         onContextMenu={(e)=>onContextMenu && onContextMenu(e, request)}
         style={{cursor: draggable ? 'grab' : 'pointer'}}>
      <div className="row" style={{gap:6}}>
        <Priority value={request.priority}/>
        <span className="mono dim" style={{fontSize:11}}>{request.reference}</span>
        <div className="spacer"/>
        {request.attachments.length > 0 && <span className="dim"><I.paper size={12}/></span>}
        {request.comments.length > 0 &&
          <span className="dim row" style={{gap:3,fontSize:11}}><I.comment size={12}/>{request.comments.length}</span>}
      </div>
      <div className="ttl">{request.title}</div>
      <div className="row wrap" style={{gap:6}}>
        {assignee
          ? <span className="row" style={{gap:5,fontSize:11.5}} title={assignee.name}>
              <PartyAvatar resolved={assignee} size="sm"/>
              <span className="truncate" style={{maxWidth:100}}>{assignee.name}</span></span>
          : <span className="xsmall dim">Unassigned</span>}
        <div className="spacer"/>
        {request.targetDate &&
          <span className="xsmall" style={overdue ? {color:'var(--red)',fontWeight:650} : {color:'var(--text-3)'}}>
            {fmtDate(request.targetDate)}</span>}
      </div>
    </div>
  );
}

function RequestBoard({requests, idx, statuses, act, onOpen, onContextMenu, canEdit}){
  const [dragId, setDragId] = useState(null);
  const [overCol, setOverCol] = useState(null);

  const drop = async (status) => {
    setOverCol(null);
    const id = dragId;
    setDragId(null);
    if (!id) return;
    const req = requests.find(r => r.id === id);
    if (!req || req.status === status) return;
    if (!canEdit(id)) return;
    await act(API.put(`/api/requests/${id}/status`, {status}), `Moved to ${status}`);
  };

  return (
    <div className="board">
      {statuses.map(status => {
        const items = requests.filter(r => r.status === status);
        return (
          <div key={status}
               className={'board-col' + (overCol === status ? ' over' : '')}
               onDragOver={e=>{ e.preventDefault(); setOverCol(status); }}
               onDragLeave={()=>setOverCol(c => c === status ? null : c)}
               onDrop={()=>drop(status)}>
            <div className="board-col-head">
              <Badge tone={REQUEST_STATUS_TONE[status] || 'slate'} dot>{status}</Badge>
              <div className="spacer"/>
              <span className="xsmall dim">{items.length}</span>
            </div>
            <div className="board-list">
              {items.map(r =>
                <RequestCard key={r.id} request={r} idx={idx} onOpen={onOpen} onContextMenu={onContextMenu}
                  draggable={canEdit(r.id)} dragging={dragId === r.id}
                  onDragStart={()=>setDragId(r.id)}/>)}
              {!items.length && <div className="xsmall dim" style={{padding:'12px 4px',textAlign:'center'}}>Nothing here</div>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ------------------------------------------------------------------- list */

function RequestList({requests, idx, onOpen, onContextMenu, hasFilters, clearFilters}){
  if (!requests.length) return <div className="card">
    <EmptyState icon={<I.inbox size={28}/>} title="No requests match"
      action={hasFilters ? <button className="btn sm" onClick={clearFilters}>Clear filters</button> : null}/></div>;
  const groups = {};
  requests.forEach(r => { (groups[r.priority] = groups[r.priority] || []).push(r); });
  return (
    <div className="stack">
      {['P1','P2','P3','P4'].filter(p => groups[p]).map(p => (
        <div className="card" key={p}>
          <div className="card-head">
            <Priority value={p}/><h3>{p === 'P1' ? 'Critical' : p === 'P2' ? 'High' : p === 'P3' ? 'Normal' : 'Low'}</h3>
            <Badge tone="slate">{groups[p].length}</Badge>
          </div>
          <div>
            {groups[p].map(r => {
              const assignee = resolveParty(r.assignee, idx);
              const overdue = isOverdue(r.targetDate) && !isClosedRequest(r);
              return (
                <div className="feed-row" key={r.id} onClick={()=>onOpen(r)}
                     onContextMenu={(e)=>onContextMenu && onContextMenu(e, r)}>
                  <span style={{marginTop:2}}>
                    <Badge tone={REQUEST_STATUS_TONE[r.status] || 'slate'} dot>{r.status}</Badge></span>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontWeight:600}}>
                      <span className="mono dim" style={{marginRight:7}}>{r.reference}</span>{r.title}</div>
                    <div className="cell-sub truncate">{r.description}</div>
                    <div className="row wrap" style={{gap:8,marginTop:6}}>
                      <Badge tone="slate">{r.requestType}</Badge>
                      {r.businessArea && <span className="xsmall dim">{r.businessArea}</span>}
                      {(r.componentIds||[]).slice(0,3).map(cid => idx.comp[cid]
                        ? <span key={cid} className="badge accent">{idx.comp[cid].identifier}</span> : null)}
                    </div>
                  </div>
                  <div style={{textAlign:'right'}}>
                    {assignee
                      ? <div className="row" style={{gap:6,justifyContent:'flex-end'}}>
                          <PartyAvatar resolved={assignee} size="sm"/>
                          <span className="small">{assignee.name}</span></div>
                      : <span className="xsmall dim">Unassigned</span>}
                    {r.targetDate &&
                      <div className="xsmall" style={overdue ? {color:'var(--red)',fontWeight:650,marginTop:4} : {marginTop:4,color:'var(--text-3)'}}>
                        target {fmtDate(r.targetDate)}</div>}
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      ))}
    </div>
  );
}

/* --------------------------------------------------------------- timeline */

function RequestTimeline({requests, idx, onOpen, onContextMenu}){
  const dated = requests.filter(r => r.startDate || r.targetDate);
  if (!dated.length) return <div className="card">
    <EmptyState icon={<I.timeline size={28}/>} title="Nothing to plot"
      body="Requests need a start or target date to appear on the timeline."/></div>;

  const times = [];
  dated.forEach(r => {
    if (r.startDate) times.push(new Date(r.startDate + 'T00:00:00').getTime());
    if (r.targetDate) times.push(new Date(r.targetDate + 'T00:00:00').getTime());
  });
  times.push(Date.now());
  const min = new Date(Math.min(...times));
  const max = new Date(Math.max(...times));
  min.setDate(1); min.setHours(0,0,0,0);
  max.setMonth(max.getMonth() + 1, 1); max.setHours(0,0,0,0);
  const span = max.getTime() - min.getTime();
  const pct = (t) => ((t - min.getTime()) / span) * 100;

  const months = [];
  const cursor = new Date(min);
  while (cursor < max) {
    months.push(new Date(cursor));
    cursor.setMonth(cursor.getMonth() + 1);
  }

  const colour = (r) => isClosedRequest(r) ? 'var(--slate)'
    : r.priority === 'P1' ? 'var(--red)'
    : r.priority === 'P2' ? 'var(--amber)'
    : r.priority === 'P3' ? 'var(--blue)' : 'var(--slate)';

  const sorted = [...dated].sort((a,b) =>
    (a.startDate || a.targetDate).localeCompare(b.startDate || b.targetDate));

  return (
    <div className="card">
      <div className="card-head"><h3>Timeline</h3><Badge tone="slate">{dated.length}</Badge>
        <span className="xsmall dim">Bars run from start date to target date. Bar colour is priority.</span></div>
      <div className="timeline">
        <div className="tl-grid">
          <div className="tl-head">
            <div className="tl-label" style={{flex:'0 0 230px',padding:'8px 12px',fontSize:11,
              textTransform:'uppercase',letterSpacing:'.06em',color:'var(--text-3)',fontWeight:650}}>Request</div>
            <div style={{flex:1,display:'flex'}}>
              {months.map((m,i) =>
                <div className="tl-month" key={i}>{m.toLocaleDateString('en-GB',{month:'short',year:'2-digit'})}</div>)}
            </div>
          </div>
          {sorted.map(r => {
            const s = r.startDate ? new Date(r.startDate + 'T00:00:00').getTime()
                                  : new Date(r.targetDate + 'T00:00:00').getTime();
            const e = r.targetDate ? new Date(r.targetDate + 'T00:00:00').getTime() : s;
            const left = pct(Math.min(s, e));
            const width = Math.max(1.4, pct(Math.max(s, e)) - left);
            return (
              <div className="tl-row" key={r.id}>
                <div className="tl-label">
                  <div className="truncate" style={{fontWeight:600}}>{r.title}</div>
                  <div className="cell-sub mono">{r.reference}</div>
                </div>
                <div className="tl-track">
                  {months.map((m,i) => i === 0 ? null :
                    <div className="tl-gridline" key={i} style={{left: pct(m.getTime()) + '%'}}/>)}
                  <div className="tl-today" style={{left: pct(Date.now()) + '%'}}/>
                  <div className="tl-bar" style={{left: left + '%', width: width + '%', background: colour(r)}}
                       onClick={()=>onOpen(r)}
                       onContextMenu={(e)=>onContextMenu && onContextMenu(e, r)}
                       title={`${r.reference} ${r.title}\n${fmtDate(r.startDate) || 'no start'} → ${fmtDate(r.targetDate) || 'no target'}`}>
                    {r.priority} · {r.status}
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

/* --------------------------------------------------------------- calendar */

function RequestCalendar({requests, idx, onOpen, onContextMenu}){
  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 first = new Date(year, month, 1);
  const startDow = (first.getDay() + 6) % 7;          // Monday first
  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 byDate = {};
  requests.forEach(r => {
    if (!r.targetDate) return;
    (byDate[r.targetDate] = byDate[r.targetDate] || []).push(r);
  });
  const iso = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  const today = todayIso();
  const withTarget = requests.filter(r => r.targetDate).length;

  const tone = (r) => isClosedRequest(r)
    ? {background:'var(--slate-soft)', color:'var(--slate)', borderColor:'var(--slate-border)'}
    : r.priority === 'P1' ? {background:'var(--red-soft)', color:'var(--red)', borderColor:'var(--red-border)'}
    : r.priority === 'P2' ? {background:'var(--amber-soft)', color:'var(--amber)', borderColor:'var(--amber-border)'}
    : {background:'var(--blue-soft)', color:'var(--blue)', borderColor:'var(--blue-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">{withTarget} of {requests.length} shown have a target date</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] || [];
            return (
              <div key={i} className={'cal-cell' + (d.getMonth() !== month ? ' other' : '') + (key === today ? ' today' : '')}>
                <div className="cal-date">{d.getDate()}</div>
                {items.slice(0, 4).map(r =>
                  <div key={r.id} className="cal-item" style={tone(r)} onClick={()=>onOpen(r)}
                       onContextMenu={(e)=>onContextMenu && onContextMenu(e, r)}
                       title={`${r.reference} ${r.title}`}>
                    {r.reference} {r.title}</div>)}
                {items.length > 4 && <div className="xsmall dim">+{items.length - 4} more</div>}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

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

/** Full record page for a business request, mirroring ComponentDetail's layout. */
function RequestDetail({id, state, idx, go, act, toast, user, canEdit}){
  const [comment, setComment] = useState('');
  const [commentKey, setCommentKey] = useState(0);
  const [internal, setInternal] = useState(false);
  const [busy, setBusy] = useState(false);
  const [raisingChange, setRaisingChange] = useState(false);
  const [editing, setEditing] = useState(null);
  const r = state.requests.find(x => x.id === id);

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

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

  const editable = canEdit(r.id);
  const assignee = resolveParty(r.assignee, idx);
  const requester = idx.person[r.requesterPersonId];
  const overdue = isOverdue(r.targetDate) && !isClosedRequest(r);
  const linked = (r.componentIds||[]).map(cid => idx.comp[cid]).filter(Boolean);
  const changes = state.changes.filter(ch => (ch.requestIds||[]).includes(r.id));
  const base = `/api/requests/${r.id}`;

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

  return (
    <div>
      <div className="page-head">
        <div className="type-icon"><I.inbox size={20}/></div>
        <div style={{minWidth:0,flex:1}}>
          <div className="row wrap" style={{gap:9,marginBottom:2}}>
            <h1>{r.title}</h1>
            <Priority value={r.priority}/>
            <Badge tone={REQUEST_STATUS_TONE[r.status] || 'slate'} dot>{r.status}</Badge>
            {overdue && <Badge tone="red">Past target</Badge>}
            {!editable && <Badge tone="slate">Read only</Badge>}
          </div>
          <div className="small muted">
            <span className="mono">{r.reference}</span> · {r.requestType}
            {r.businessArea && <> · {r.businessArea}</>}
            {r.updatedAt && <> · updated {relative(r.updatedAt)} by {r.updatedBy}</>}
          </div>
        </div>
        {editable && <button className="btn" onClick={()=>setEditing(r)}><I.edit size={14}/>Edit</button>}
      </div>

      <div className="detail">
        <div>
          {editable && !isClosedRequest(r) &&
            <div className="row wrap" style={{gap:6, marginBottom:16}}>
              <span className="xsmall dim" style={{marginRight:4}}>Move to</span>
              {(state.requestStatuses||[]).filter(s => s !== r.status).map(s =>
                <button key={s} className="chip-toggle" disabled={busy}
                  onClick={async ()=>{ setBusy(true); await act(API.put(`${base}/status`, {status:s}), `Moved to ${s}`); setBusy(false); }}>
                  {s}</button>)}
            </div>}

          {r.description
            ? <RichText value={r.description}/>
            : <p className="dim" style={{marginTop:0}}>No description given.</p>}

          {r.resolution &&
            <><div className="sub-head">Resolution</div>
              <p className="pre-wrap muted" style={{margin:0}}>{r.resolution}</p></>}

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

          {/* A request says what the business wants; a change says what will be done
              about it. Raising one here carries the context across and links them. */}
          <div className="sub-head">Changes</div>
          {changes.length
            ? <div className="stack" style={{gap:8}}>
                {changes.map(ch => (
                  <div className="row" key={ch.id} style={{gap:9,padding:'9px 12px',
                    border:'1px solid var(--border)',borderRadius:'var(--radius-sm)',cursor:'pointer'}}
                    onClick={()=>go({view:'change', id:ch.id})}>
                    <span className="mono dim" style={{fontSize:11.5}}>{ch.reference}</span>
                    <span style={{flex:1,minWidth:0,fontWeight:600}} className="truncate">{ch.title}</span>
                    <Badge tone={CHANGE_STATUS_TONE[ch.status] || 'slate'} dot>{ch.status}</Badge>
                  </div>))}
              </div>
            : <p className="dim small" style={{margin:0}}>No changes have been raised from this request yet.</p>}
          <button className="btn sm" style={{marginTop:10}} onClick={()=>setRaisingChange(true)}>
            <I.change size={13}/>Raise a change from this request</button>

          <div className="sub-head">Conversation</div>
          <div>
            {r.comments.length === 0 && <p className="dim small">No comments yet.</p>}
            {r.comments.map(c => (
              <div className={'comment' + (c.isInternal ? ' internal' : '')} key={c.id}>
                <Avatar name={c.createdBy} size="sm"/>
                <div style={{flex:1,minWidth:0}}>
                  <div className="row" style={{gap:7}}>
                    <strong style={{fontSize:13}}>{c.createdBy}</strong>
                    <span className="xsmall dim">{fmtDateTime(c.createdAt)}</span>
                    {c.isInternal && <Badge tone="amber">Internal</Badge>}
                  </div>
                  <RichText value={c.body} className="comment-body"/>
                </div>
              </div>
            ))}
          </div>
          <div style={{marginTop:12}}>
            <RichTextEditor key={commentKey} mode="compact" value={comment} onChange={setComment} toast={toast}
              uploadContext={{entityKind:'request', entityId:r.id}} placeholder="Add a comment…"/>
            <div className="row" style={{marginTop:8}}>
              {state.permissions.editableRequestIds === '*' &&
                <label className="row" style={{gap:7,cursor:'pointer'}}>
                  <input type="checkbox" checked={internal} onChange={e=>setInternal(e.target.checked)}/>
                  <span className="small">Internal note</span></label>}
              <div className="spacer"/>
              <button className="btn primary sm" disabled={rteIsBlank(comment) || busy} onClick={postComment}>
                <I.comment size={13}/>Comment</button>
            </div>
          </div>

          {r.events.length > 0 &&
            <><div className="sub-head">History</div>
              <div className="side-list">
                {r.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>
                    </span>
                  </div>)}
              </div></>}

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

        <div className="stack">
          <div className="card">
            <div className="card-head"><h3>Request details</h3></div>
            <div className="side-list">
              <div className="side-row">
                <span className="k">Raised by</span>
                <span className="v">{requester ? requester.name : r.raisedBy}</span>
              </div>
              <div className="side-row">
                <span className="k">Assigned to</span>
                <span className="v">{assignee
                  ? <span className="row" style={{gap:7,justifyContent:'flex-end'}}>
                      <PartyAvatar resolved={assignee} size="sm"/>{assignee.name}</span>
                  : <span className="dim">Unassigned</span>}</span>
              </div>
              <div className="side-row"><span className="k">Start date</span>
                <span className="v">{fmtDate(r.startDate) || '—'}</span></div>
              <div className="side-row"><span className="k">Target date</span>
                <span className="v">{r.targetDate
                  ? <span style={overdue ? {color:'var(--red)',fontWeight:600} : null}>{fmtDate(r.targetDate)}</span>
                  : '—'}</span></div>
              <div className="side-row"><span className="k">Effort estimate</span>
                <span className="v">{r.effortEstimate ? `${r.effortEstimate} day${r.effortEstimate===1?'':'s'}` : 'Not sized'}</span></div>
              {r.closedAt &&
                <div className="side-row"><span className="k">Closed</span><span className="v">{fmtDateTime(r.closedAt)}</span></div>}
            </div>
          </div>
        </div>
      </div>

      {raisingChange &&
        <ChangeForm state={state} onClose={()=>setRaisingChange(false)}
          value={NEW_CHANGE({
            fromRequestId: r.id,
            title: `${r.reference}: ${r.title}`,
            description: r.description,
            reason: `Raised from business request ${r.reference}.`,
            requesterPersonId: r.requesterPersonId || '',
            componentIds: [...(r.componentIds || [])],
            requestIds: [r.id]
          })}
          onSave={async (v)=>{
            const ok = await act(API.post('/api/changes', v), 'Change raised and linked');
            if (ok) setRaisingChange(false);
          }}/>}

      {editing &&
        <RequestForm value={editing} state={state} user={user} onClose={()=>setEditing(null)} toast={toast}
          onSave={async (v)=>{
            const ok = await act(API.put(`/api/requests/${v.id}`, v), 'Request updated');
            if (ok) setEditing(null);
          }}/>}
    </div>
  );
}

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

function RequestForm({value, state, user, onClose, onSave, toast}){
  const [r, setR] = useState(()=>clone(value));
  const [busy, setBusy] = useState(false);
  const set = (k,v) => setR(x => ({...x, [k]:v}));
  const isNew = !value.id;
  const canTriage = state.permissions.editableRequestIds === '*';

  return (
    <Modal wide title={isNew ? 'Raise a business request' : `Edit ${value.reference}`} onClose={onClose}
      footer={<>
        <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
        <button className="btn primary" disabled={!r.title.trim() || busy}
          onClick={async ()=>{ setBusy(true); try { await onSave(r); } finally { setBusy(false); } }}>
          {busy ? 'Saving…' : isNew ? 'Raise request' : 'Save changes'}</button>
      </>}>
      {isNew && !canTriage &&
        <div className="info-box" style={{marginBottom:16}}>
          Your request will be raised as New. The team will triage it, set a priority and assign it.
        </div>}

      <Field label="Title"><input type="text" value={r.title} autoFocus
        onChange={e=>set('title', e.target.value)} placeholder="Summarise the request in one line"/></Field>
      <Field label="Description" hint="What do you need, and why? Include anything that would help whoever picks this up.">
        <RichTextEditor value={r.description} onChange={v=>set('description', v)} toast={toast}
          uploadContext={r.id ? {entityKind:'request', entityId:r.id} : null}/>
      </Field>

      <div className="form-grid three">
        <Field label="Type">
          <select value={r.requestType} onChange={e=>set('requestType', e.target.value)}>
            {(state.requestTypes||[]).map(t => <option key={t}>{t}</option>)}</select></Field>
        <Field label="Business area">
          <select value={r.businessArea} onChange={e=>set('businessArea', e.target.value)}>
            <option value="">Not set</option>
            {(state.lookups.business_area||[]).map(t => <option key={t}>{t}</option>)}</select></Field>
        <Field label="Priority" hint={canTriage ? '' : 'Set during triage'}>
          <select value={r.priority} disabled={!canTriage && isNew}
            onChange={e=>set('priority', e.target.value)}>
            {(state.requestPriorities||[]).map(t => <option key={t}>{t}</option>)}</select></Field>
      </div>

      {(canTriage || !isNew) &&
        <>
          <div className="sub-head">Triage</div>
          <div className="form-grid">
            <Field label="Status">
              <select value={r.status} onChange={e=>set('status', e.target.value)}>
                {(state.requestStatuses||[]).map(t => <option key={t}>{t}</option>)}</select></Field>
            <Field label="Assigned to">
              <PartySelect value={r.assignee} state={state} onChange={v=>set('assignee', v)}
                placeholder="Unassigned"/></Field>
            <Field label="Requested by">
              <select value={r.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>
            <Field label="Effort estimate" hint="Working days">
              <input type="number" min="0" step="0.5" value={r.effortEstimate}
                onChange={e=>set('effortEstimate', e.target.value)}/></Field>
            <Field label="Start date"><input type="date" value={r.startDate}
              onChange={e=>set('startDate', e.target.value)}/></Field>
            <Field label="Target date"><input type="date" value={r.targetDate}
              onChange={e=>set('targetDate', e.target.value)}/></Field>
          </div>
          <Field label="Resolution" hint="Filled in when the request is closed.">
            <textarea value={r.resolution} onChange={e=>set('resolution', e.target.value)}/></Field>
        </>}

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