// crm-tickets.jsx — Help Desk Ticketing
const { useState, useEffect, useCallback } = React;

// ── Custom ticket fields (definitions live in SystemConfig 'ticket.customFields') ──
// Definitions: [{ key, label, options:[…] }]; per-ticket values ride on
// ticket.customFields as a JSON string like '{"software":"TimeSuite"}'.
function useCustomFieldDefs() {
  const [defs, setDefs] = useState([]);
  useEffect(() => {
    CRM.ConfigAPI.getAll().then(all => {
      try {
        const d = JSON.parse(all['ticket.customFields'] || '[]');
        setDefs(Array.isArray(d) ? d : []);
      } catch { setDefs([]); }
    }).catch(() => {});
  }, []);
  return defs;
}

function parseTicketCustomFields(raw) {
  try {
    const v = JSON.parse(raw || '{}');
    return (v && typeof v === 'object' && !Array.isArray(v)) ? v : {};
  } catch { return {}; }
}

// CC'd tech ids ride on ticket.ccTechIds as a JSON array string.
function parseCcTechIds(raw) {
  try {
    const v = JSON.parse(raw || '[]');
    return Array.isArray(v) ? v.filter(x => typeof x === 'string') : [];
  } catch { return []; }
}

// Multi-select of techs to CC — chips for the selected, dropdown to add.
function CcPicker({ techs, value, onChange, exclude }) {
  const available = techs.filter(t => !value.includes(t.id) && t.id !== exclude);
  return (
    <div style={{display:'flex',flexWrap:'wrap',gap:6,alignItems:'center'}}>
      {value.map(id => {
        const t = techs.find(x => x.id === id);
        return (
          <span key={id} style={{display:'inline-flex',alignItems:'center',gap:6,background:'var(--off)',
            border:'1px solid var(--g200)',borderRadius:999,padding:'3px 10px',fontSize:12,fontWeight:600,color:'var(--navy)'}}>
            {t?.name || '?'}
            <button onClick={() => onChange(value.filter(x => x !== id))}
              style={{border:'none',background:'none',cursor:'pointer',color:'var(--g400)',fontSize:13,padding:0,lineHeight:1}}>×</button>
          </span>
        );
      })}
      {available.length > 0 && (
        <select className="search-input" style={{width:150,fontSize:12,padding:'4px 8px'}} value=""
          onChange={e => { if (e.target.value) onChange([...value, e.target.value]); }}>
          <option value="">+ CC a tech…</option>
          {available.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
        </select>
      )}
      {value.length === 0 && available.length === 0 && (
        <span style={{fontSize:12,color:'var(--g400)'}}>No other techs to CC.</span>
      )}
    </div>
  );
}

// JSON string for the API — drops empty values; null when nothing is set.
function serializeTicketCustomFields(values) {
  const clean = {};
  Object.entries(values || {}).forEach(([k, v]) => { if (v) clean[k] = v; });
  return Object.keys(clean).length ? JSON.stringify(clean) : null;
}

const STAT_BADGE  = { new:'badge-purple', open:'badge-blue', 'in-progress':'badge-purple', pending:'badge-amber', closed:'badge-gray' };
const PRI_BADGE   = { low:'badge-gray', medium:'badge-amber', high:'badge-red', urgent:'badge-red' };
const PRI_ORDER   = { urgent:0, high:1, medium:2, low:3 };

function Tickets({ user, showToast, quickAdd }) {
  const [tickets, setTickets] = useState([]);
  const [mainTab, setMainTab] = useState('active');   // 'active' (new/open/pending, by tech) | 'closed'
  const [priFilter, setPriFilter] = useState('');
  const [search, setSearch] = useState('');
  const [selected, setSelected] = useState(null);
  const [showAdd, setShowAdd] = useState(false);
  const [showImport, setShowImport] = useState(false);
  const [showLinker, setShowLinker] = useState(false);   // company-linking tool
  const [loading, setLoading] = useState(false);
  const [zdImport, setZdImport] = useState(null);   // progress object while a Zendesk import runs
  const [statusCounts, setStatusCounts] = useState(null); // true totals per status (from stats)
  const [groupLimits, setGroupLimits] = useState({});     // per-group render cap (keeps the DOM small)
  const [selIds, setSelIds] = useState({});                // admin multi-select for bulk delete
  // Column sorting + drag-reorder (order persists per browser)
  const [sortCol, setSortCol] = useState(null);
  const [sortDir, setSortDir] = useState('asc');
  const [colOrder, setColOrder] = useState(() => {
    try { const v = JSON.parse(localStorage.getItem('tktColOrder') || 'null'); return Array.isArray(v) ? v : null; }
    catch { return null; }
  });
  const dragColRef = React.useRef(null);
  const customFieldDefs = useCustomFieldDefs();

  const GROUP_CAP = 50;   // rows rendered per group before "show more"
  const capOf = key => groupLimits[key] || GROUP_CAP;
  const bumpCap = key => setGroupLimits(l => ({ ...l, [key]: capOf(key) + 100 }));

  useEffect(() => { if (quickAdd) setShowAdd(true); }, [quickAdd]);

  // Poll an in-flight Zendesk import (either just started here, or already
  // running when the view mounted) until it finishes, then refresh the list.
  const pollZendesk = useCallback(() => {
    let stop = false;
    const tick = async () => {
      if (stop) return;
      try {
        const s = await CRM.TicketsAPI.importStatus();
        setZdImport(s);
        if (s.running) { setTimeout(tick, 3000); return; }
        // finished
        if (s.finishedAt) {
          await load();
          if (s.fatalError) showToast('Zendesk import failed: ' + s.fatalError, 'error');
          else showToast(`Zendesk import done — ${s.imported} imported, ${s.updated} updated.`, 'success');
        }
      } catch { setTimeout(tick, 5000); }
    };
    tick();
    return () => { stop = true; };
  }, [load]);

  // Resume the banner if an import is already running when we arrive.
  useEffect(() => {
    CRM.TicketsAPI.importStatus().then(s => { if (s.running) { setZdImport(s); pollZendesk(); } }).catch(() => {});
  }, []);

  const runZendeskImport = async () => {
    const email = (prompt('Zendesk admin email:') || '').trim();
    if (!email) return;
    const token = (prompt('Zendesk API token (Admin Center → Apps and integrations → Zendesk API):') || '').trim();
    if (!token) { showToast('No API token entered — import cancelled.', 'error'); return; }
    if (!confirm('Import ALL past tickets from ctrny.zendesk.com?\n\nThis pulls the full history with comments, attachments and custom fields. It runs in the background and can take several minutes.')) return;
    try {
      await CRM.TicketsAPI.importZendesk('ctrny.zendesk.com', email, token);
      setZdImport({ running: true, phase: 'Starting…', ticketsSeen: 0, imported: 0, updated: 0 });
      pollZendesk();
    } catch (e) {
      showToast('Could not start import: ' + e.message, 'error');
    }
  };

  const load = useCallback(async () => {
    setLoading(true);
    try {
      const filters = {};
      if (mainTab === 'closed')   { filters.status = 'closed'; filters.limit = 1000; }
      if (priFilter)              filters.priority = priFilter;
      let all = await CRM.TicketsAPI.getAll(filters);
      all.sort((a,b) => (PRI_ORDER[a.priority]??9) - (PRI_ORDER[b.priority]??9) || new Date(b.updatedAt)-new Date(a.updatedAt));
      setTickets(all);
    } catch (e) {
      showToast('Failed to load tickets: ' + e.message, 'error');
    } finally { setLoading(false); }
    // True per-status totals for the tabs/chips — the loaded list is filtered
    // and capped, so it can't be counted for the other buckets.
    CRM.TicketsAPI.stats()
      .then(s => { const m = {}; (s.byStatus||[]).forEach(x => m[x.status] = x.count); setStatusCounts(m); })
      .catch(() => {});
  }, [mainTab, priFilter]);

  useEffect(() => { load(); }, [load]);

  // Prefer true totals from the stats endpoint; fall back to counting the
  // loaded (active) set until they arrive.
  const statCounts = statusCounts || CRM.TICKET_STATUSES.reduce((acc, s) => {
    acc[s.id] = tickets.filter(t => t.status === s.id).length;
    return acc;
  }, {});

  // Client-side search over the loaded set (subject / account).
  const q = search.trim().toLowerCase();
  const visible = q
    ? tickets.filter(t =>
        (t.subject || '').toLowerCase().includes(q) ||
        (t.account?.companyName || '').toLowerCase().includes(q))
    : tickets;

  // Group active tickets by assignee — Unassigned (triage) first, then techs A→Z.
  // `tickets` is already sorted (priority desc, updatedAt desc) so groups keep order.
  const unassigned = visible.filter(t => !t.assignedTechId && !t.assignedTech);
  const byTech = {};
  visible.filter(t => t.assignedTechId || t.assignedTech).forEach(t => {
    const name = t.assignedTech?.name || CRM.UsersAPI.getById(t.assignedTechId)?.name || 'Unknown';
    (byTech[name] = byTech[name] || []).push(t);
  });
  const techNames = Object.keys(byTech).sort((a, b) => a.localeCompare(b));

  const sectionHeader = (label, count, red) => (
    <div style={{fontSize:11,fontWeight:700,letterSpacing:'.06em',textTransform:'uppercase',color:red?'#BC141E':'var(--g400)',margin:'18px 0 8px'}}>
      {label} ({count})
    </div>
  );

  const dot = (color) => <span style={{display:'inline-block',width:8,height:8,borderRadius:'50%',background:color,marginRight:6,verticalAlign:'middle'}} />;

  const assignableUsers = CRM.UsersAPI.getAll().filter(u => u.isActive !== false);

  const reassign = async (t, techId) => {
    try {
      await CRM.TicketsAPI.assign(t.id, techId || null);
      await load();
      showToast(techId ? 'Ticket reassigned.' : 'Ticket unassigned.', 'success');
    } catch (e) { showToast('Reassign failed: ' + e.message, 'error'); }
  };

  const selCount = Object.keys(selIds).length;
  const bulkDelete = async () => {
    if (!confirm('Delete ' + selCount + ' selected ticket(s) permanently? This cannot be undone.')) return;
    try {
      const r = await CRM.TicketsAPI.bulkDelete(Object.keys(selIds));
      setSelIds({});
      await load();
      showToast((r.deleted || 0) + ' ticket(s) deleted.', 'success');
    } catch (e) { showToast('Bulk delete failed: ' + e.message, 'error'); }
  };

  const markSpam = async (t) => {
    if (!t.requesterEmail) return showToast('This ticket has no requester email to block.', 'error');
    if (!confirm('Block ' + t.requesterEmail + ' as spam and delete this ticket?\n\nFuture emails from this sender will never become tickets.')) return;
    try {
      await CRM.TicketsAPI.spamBlock(t.requesterEmail, t.id);
      await load();
      showToast(t.requesterEmail + ' blocked.', 'success');
    } catch (e) { showToast('Spam block failed: ' + e.message, 'error'); }
  };

  const deleteTicket = async (t) => {
    if (!confirm(`Delete ticket #${t.ticketNumber ?? ''} "${t.subject}" permanently? This cannot be undone.`)) return;
    try {
      await CRM.TicketsAPI.delete(t.id);
      await load();
      showToast('Ticket deleted.', 'success');
    } catch (e) { showToast('Delete failed: ' + e.message, 'error'); }
  };

  // Excel-style ticket table with sortable, drag-reorderable columns.
  // Order persists in localStorage; checkbox + actions columns stay fixed.
  // Platform stays on the ticket detail but is dropped from the list columns
  const listDefs = customFieldDefs.filter(d => (d.key || '').toLowerCase() !== 'platform');

  const COLS = [
    { k:'num',      label:'#',        th:{width:64},                 sort:t => t.ticketNumber || 0,
      cell:t => <td key="num" style={{fontFamily:'monospace',fontWeight:700,color:'var(--g600)'}}>{t.ticketNumber ?? '—'}</td> },
    { k:'status',   label:'Status',   th:{width:104},                sort:t => t.status,
      cell:t => <td key="status"><span className={`badge ${STAT_BADGE[t.status]||'badge-gray'}`} style={{textTransform:'capitalize',whiteSpace:'nowrap'}}>{t.status.replace('-',' ')}</span></td> },
    { k:'priority', label:'Priority', th:{width:88},                 sort:t => PRI_ORDER[t.priority] ?? 9,
      cell:t => <td key="priority" style={{whiteSpace:'nowrap'}}>{dot(CRM.TICKET_PRIORITIES.find(p=>p.id===t.priority)?.color || 'var(--g200)')}<span style={{textTransform:'capitalize',fontSize:12}}>{t.priority}</span></td> },
    { k:'subject',  label:'Subject',  th:{minWidth:220},             sort:t => t.subject || '',
      cell:t => {
        const stale = t.status !== 'closed' && new Date(t.updatedAt) < new Date(Date.now()-3*864e5);
        const ccCount = parseCcTechIds(t.ccTechIds).length;
        return (
          <td key="subject" style={{fontWeight:600,color:'var(--navy)'}}>
            {t.subject}
            {stale && <span className="badge badge-red" style={{marginLeft:6}}>⚠ Stale</span>}
            {ccCount > 0 && <span className="badge badge-gray" style={{marginLeft:6}} title="CC'd technicians">cc {ccCount}</span>}
          </td>
        );
      } },
    { k:'account',  label:'Account',  th:{minWidth:120},             sort:t => t.account?.companyName || '',
      cell:t => <td key="account" style={{color:'var(--g600)'}}>{t.account?.companyName || '—'}</td> },
    ...listDefs.map(d => ({
      k:'cf:'+d.key, label:d.label, th:{whiteSpace:'nowrap'},        sort:t => parseTicketCustomFields(t.customFields)[d.key] || '',
      cell:t => <td key={'cf:'+d.key} style={{color:'var(--g600)',fontSize:12,whiteSpace:'nowrap'}}>{parseTicketCustomFields(t.customFields)[d.key] || '—'}</td> })),
    { k:'tech',     label:'Technician', th:{minWidth:110}, techOnly:true, sort:t => t.assignedTech?.name || CRM.UsersAPI.getById(t.assignedTechId)?.name || '',
      cell:t => <td key="tech" style={{color:'var(--g600)'}}>{t.assignedTech?.name || CRM.UsersAPI.getById(t.assignedTechId)?.name || '—'}</td> },
    { k:'updated',  label:'Updated',  th:{width:94,whiteSpace:'nowrap'}, sort:t => t.updatedAt || '',
      cell:t => <td key="updated" style={{color:'var(--g400)',fontSize:12,whiteSpace:'nowrap'}}>{new Date(t.updatedAt).toLocaleDateString()}</td> },
  ];
  const defaultOrder = COLS.map(c => c.k);
  const order = [...(colOrder || defaultOrder).filter(k => defaultOrder.includes(k)),
                 ...defaultOrder.filter(k => !(colOrder || defaultOrder).includes(k))];
  const saveOrder = (next) => { setColOrder(next); try { localStorage.setItem('tktColOrder', JSON.stringify(next)); } catch {} };
  const toggleSort = (k) => {
    if (sortCol === k) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    else { setSortCol(k); setSortDir('asc'); }
  };

  const ticketTable = (list, opts = {}) => {
    const cols = order.map(k => COLS.find(c => c.k === k)).filter(c => c && (!c.techOnly || opts.showTech));
    return (
    <div style={{overflowX:'auto',border:'1px solid var(--g200)',borderRadius:8,background:'white'}}>
      <table className="tbl tkt-tbl" style={{margin:0,minWidth:760}}>
        <thead><tr>
          {user.isAdmin && <th style={{width:30}}></th>}
          {cols.map(c => (
            <th key={c.k} style={{...c.th,cursor:'pointer',userSelect:'none'}}
              draggable
              title="Click to sort · drag to reorder"
              onClick={() => toggleSort(c.k)}
              onDragStart={e => { dragColRef.current = c.k; e.dataTransfer.effectAllowed = 'move'; }}
              onDragOver={e => e.preventDefault()}
              onDrop={e => {
                e.preventDefault();
                const from = dragColRef.current; dragColRef.current = null;
                if (!from || from === c.k) return;
                const next = order.filter(k => k !== from);
                next.splice(next.indexOf(c.k), 0, from);
                saveOrder(next);
              }}>
              {c.label}
              <span style={{marginLeft:4,fontSize:9,opacity: sortCol === c.k ? .85 : .18}}>
                {sortCol === c.k && sortDir === 'desc' ? '▼' : '▲'}
              </span>
            </th>
          ))}
          <th style={{width:user.isAdmin?76:10}}></th>
        </tr></thead>
        <tbody>
          {list.map(t => (
              <tr key={t.id} onClick={()=>setSelected(t)} style={{cursor:'pointer',background: selIds[t.id] ? '#FFF7ED' : undefined}}>
                {user.isAdmin && (
                  <td onClick={e => e.stopPropagation()}>
                    <input type="checkbox" checked={!!selIds[t.id]} style={{cursor:'pointer'}}
                      onChange={e => setSelIds(m => { const n = { ...m }; if (e.target.checked) n[t.id] = true; else delete n[t.id]; return n; })} />
                  </td>
                )}
                {cols.map(c => c.cell(t))}
                <td onClick={e => e.stopPropagation()} style={{whiteSpace:'nowrap'}}>
                  {user.isAdmin && (
                    <React.Fragment>
                      {t.requesterEmail && (
                        <button className="btn btn-ghost btn-xs" style={{marginLeft:4}}
                          title={'Block ' + t.requesterEmail + ' as spam'} onClick={() => markSpam(t)}>🚫</button>
                      )}
                      <button className="btn btn-ghost btn-xs" style={{color:'#BC141E',marginLeft:2}}
                        title="Delete ticket" onClick={() => deleteTicket(t)}>🗑</button>
                    </React.Fragment>
                  )}
                </td>
              </tr>
          ))}
        </tbody>
      </table>
    </div>
    );
  };

  // Cap + "show more" wrapper so a huge group never mounts thousands of rows.
  // Column sorting applies within each group (and the closed flat table).
  const renderGroupTable = (key, list, opts) => {
    if (sortCol) {
      const c = COLS.find(x => x.k === sortCol);
      if (c) list = [...list].sort((a, b) => {
        const av = c.sort(a), bv = c.sort(b);
        const r = (typeof av === 'number' && typeof bv === 'number') ? av - bv : String(av).localeCompare(String(bv));
        return sortDir === 'asc' ? r : -r;
      });
    }
    const cap = capOf(key);
    const shown = list.slice(0, cap);
    return (
      <React.Fragment>
        {ticketTable(shown, opts)}
        {list.length > shown.length && (
          <button className="btn btn-ghost btn-sm" style={{marginTop:8}} onClick={() => bumpCap(key)}>
            Show more ({list.length - shown.length} more)
          </button>
        )}
      </React.Fragment>
    );
  };

  const activeCount = CRM.TICKET_ACTIVE_STATUSES.reduce((n, id) => n + (statCounts[id] || 0), 0);
  const closedCount = statCounts.closed || 0;

  return (
    <div>
      <style>{`
        .tkt-tbl td { padding:9px 12px; font-size:13px; border-bottom:1px solid var(--g100); vertical-align:middle; }
        .tkt-tbl tbody tr:hover { background: var(--off); }
      `}</style>

      {/* Open | Closed tabs */}
      <div style={{display:'flex',gap:4,marginBottom:16,borderBottom:'1px solid var(--g200)'}}>
        {[{id:'active',label:'🎫 Open Tickets',n:activeCount},{id:'closed',label:'✓ Closed',n:closedCount}].map(tb => (
          <button key={tb.id} onClick={()=>setMainTab(tb.id)}
            style={{border:'none',background:'none',cursor:'pointer',fontFamily:'var(--font)',fontSize:14,fontWeight:700,padding:'10px 18px',
              color: mainTab===tb.id?'var(--navy)':'var(--g400)', borderBottom: mainTab===tb.id?'3px solid var(--red)':'3px solid transparent', marginBottom:-1}}>
            {tb.label} <span style={{fontWeight:600,color:'var(--g400)'}}>({tb.n})</span>
          </button>
        ))}
      </div>

      {/* Toolbar: search + priority + actions. No status chips — the Open
          Tickets tab always shows every non-closed ticket. */}
      <div style={{display:'flex',gap:10,marginBottom:16,flexWrap:'wrap',alignItems:'center'}}>
        <div style={{marginLeft:'auto',display:'flex',gap:8,alignItems:'center',flexWrap:'wrap'}}>
          <input className="search-input" style={{width:180}} placeholder="Search subject / account…" value={search} onChange={e=>setSearch(e.target.value)} />
          <select className="search-input" style={{width:130}} value={priFilter} onChange={e=>setPriFilter(e.target.value)}>
            <option value="">All Priorities</option>
            {CRM.TICKET_PRIORITIES.map(p=><option key={p.id} value={p.id}>{p.label}</option>)}
          </select>
          <button className="btn btn-ghost btn-sm" onClick={load} disabled={loading}>{loading ? 'Refreshing…' : '↻ Refresh'}</button>
          <button className="btn btn-ghost btn-sm" onClick={() => setShowImport(true)}>⬆ Import CSV</button>
          {user.isAdmin && (
            <button className="btn btn-ghost btn-sm" onClick={runZendeskImport} disabled={zdImport?.running}>
              {zdImport?.running ? 'Importing…' : '⬇ Zendesk history'}
            </button>
          )}
          {user.isAdmin && (
            <button className="btn btn-ghost btn-sm" onClick={() => setShowLinker(true)}>🔗 Link companies</button>
          )}
          {user.isAdmin && selCount > 0 && (
            <button className="btn btn-sm" style={{background:'#BC141E',color:'#fff',border:'none'}} onClick={bulkDelete}>
              🗑 Delete selected ({selCount})
            </button>
          )}
          <button className="btn btn-primary btn-sm" onClick={() => setShowAdd(true)}>+ New Ticket</button>
        </div>
      </div>

      {/* Zendesk history import progress */}
      {zdImport && (
        <div className="card" style={{padding:'12px 16px',marginBottom:16,borderLeft:`4px solid ${zdImport.fatalError ? '#BC141E' : (zdImport.running ? '#2563EB' : '#16A34A')}`,background:'var(--off)'}}>
          <div style={{display:'flex',alignItems:'center',gap:12,flexWrap:'wrap'}}>
            <span style={{fontSize:13,fontWeight:700,color:'var(--navy)'}}>
              {zdImport.running ? '⏳ Zendesk import running' : (zdImport.fatalError ? '⚠ Zendesk import failed' : '✓ Zendesk import complete')}
            </span>
            <span style={{fontSize:12,color:'var(--g600)'}}>{zdImport.phase}</span>
            <span style={{marginLeft:'auto',fontSize:12,color:'var(--g600)'}}>
              {zdImport.ticketsSeen||0} seen · {zdImport.imported||0} imported · {zdImport.updated||0} updated
              {zdImport.commentsAdded ? ` · ${zdImport.commentsAdded} comments` : ''}
              {zdImport.attachmentsSaved ? ` · ${zdImport.attachmentsSaved} files` : ''}
            </span>
            {!zdImport.running && (
              <button className="btn btn-ghost btn-xs" onClick={() => setZdImport(null)}>Dismiss</button>
            )}
          </div>
          {zdImport.fatalError && <div style={{fontSize:12,color:'#BC141E',marginTop:6}}>{zdImport.fatalError}</div>}
          {zdImport.errors && zdImport.errors.length > 0 && (
            <details style={{marginTop:6}}>
              <summary style={{fontSize:12,color:'var(--g600)',cursor:'pointer'}}>{zdImport.errors.length} row warning(s)</summary>
              <div style={{fontSize:11,color:'var(--g600)',marginTop:4,maxHeight:120,overflow:'auto'}}>
                {zdImport.errors.map((e,i) => <div key={i}>{e}</div>)}
              </div>
            </details>
          )}
        </div>
      )}

      {loading && <div style={{padding:24,textAlign:'center',color:'var(--g400)',fontSize:13}}>Loading tickets…</div>}

      {!loading && visible.length === 0 && (
        <div className="empty"><div className="empty-icon">🎫</div><div className="empty-text">
          {search ? 'No tickets match your search.' : (mainTab==='closed' ? 'No closed tickets.' : 'No open tickets — nice.')}
        </div></div>
      )}

      {/* Active tab — grouped by technician: Unassigned triage first, then techs A→Z */}
      {mainTab==='active' && !loading && visible.length > 0 && (
        <div>
          {sectionHeader('🔺 Unassigned', unassigned.length, unassigned.length > 0)}
          {unassigned.length === 0
            ? <div style={{fontSize:12,color:'var(--g400)',padding:'4px 2px'}}>Triage queue is empty.</div>
            : renderGroupTable('__unassigned', unassigned, {})}
          {techNames.map(name => (
            <div key={name} style={{marginTop:18}}>
              {sectionHeader(`👤 ${name}`, byTech[name].length, false)}
              {renderGroupTable('tech:' + name, byTech[name], {})}
            </div>
          ))}
        </div>
      )}

      {/* Closed tab — one flat table */}
      {mainTab==='closed' && !loading && visible.length > 0 && (
        <div>
          {sectionHeader('✓ Closed', closedCount, false)}
          {closedCount > tickets.length && !search && (
            <div style={{fontSize:12,color:'var(--g400)',marginBottom:8}}>
              Showing the {tickets.length} most recently updated of {closedCount}. Use search to find older closed tickets.
            </div>
          )}
          {renderGroupTable('__closed', visible, {showTech:true})}
        </div>
      )}

      {selected && <TicketDetailModal ticket={selected} user={user}
        onClose={() => { setSelected(null); load(); }} showToast={showToast} />}
      {showImport && (
        <ImportModal type="tickets" onClose={() => setShowImport(false)}
          onDone={() => { load(); showToast('Tickets imported successfully!', 'success'); }} />
      )}
      {showLinker && (
        <CompanyLinkerModal showToast={showToast}
          onClose={() => setShowLinker(false)}
          onChanged={() => load()} />
      )}
      {showAdd && <AddTicketModal user={user} showToast={showToast} onClose={() => setShowAdd(false)}
        onSave={async data => {
          try {
            await CRM.TicketsAPI.create(data);
            await load();
            setShowAdd(false);
            showToast('Ticket created!', 'success');
          } catch (e) { showToast('Failed to create ticket: ' + e.message, 'error'); }
        }} />}
    </div>
  );
}

function TicketDetailModal({ ticket: initialTicket, user, onClose, showToast }) {
  const [ticket, setTicket] = useState(initialTicket);
  const [comments, setComments] = useState([]);
  const [commentCap, setCommentCap] = useState(30);   // bound rendered thread (newest kept visible)
  const [commentText, setCommentText] = useState('');
  const [composeMode, setComposeMode] = useState('note');   // 'note' | 'reply'
  const [sending, setSending] = useState(false);
  const [editing, setEditing] = useState(false);
  const [analyzing, setAnalyzing] = useState(false);
  const [form, setForm] = useState({...initialTicket});
  const [accounts, setAccounts] = useState([]);
  const [editContacts, setEditContacts] = useState([]);
  const [saving, setSaving] = useState(false);
  const customFieldDefs = useCustomFieldDefs();
  const [cfValues, setCfValues] = useState(() => parseTicketCustomFields(initialTicket.customFields));
  const [ccIds, setCcIds] = useState(() => parseCcTechIds(initialTicket.ccTechIds));
  // Status the ticket will move to when the composer is submitted (Zendesk-style
  // "Submit as …"). Follows the flow new → open → in progress → pending → closed.
  const [submitStatus, setSubmitStatus] = useState(initialTicket.status);

  const refresh = async () => {
    const refreshed = await CRM.TicketsAPI.getById(ticket.id);
    setTicket(refreshed);
    setForm(refreshed);
    setCfValues(parseTicketCustomFields(refreshed.customFields));
    setCcIds(parseCcTechIds(refreshed.ccTechIds));
    setSubmitStatus(refreshed.status);
    return refreshed;
  };

  const loadComments = async () => {
    try {
      const c = await CRM.TicketsAPI.getComments(ticket.id);
      setComments(c);
    } catch { /* non-critical */ }
  };

  useEffect(() => { loadComments(); }, [ticket.id]);

  // List rows are slim (no description) — pull the full ticket on open.
  useEffect(() => { refresh().catch(() => {}); }, []);

  useEffect(() => {
    // Accounts load for the edit form AND for the required company-assignment
    // banner on account-less tickets.
    if (editing || !ticket.accountId) {
      CRM.AccountsAPI.getAll(user.role === 'rep' && !user.isAdmin ? { repId: user.id } : {}).then(setAccounts).catch(() => {});
    }
    if (editing && ticket.accountId) {
      CRM.ContactsAPI.getByAccount(ticket.accountId).then(setEditContacts).catch(() => {});
    }
  }, [editing, ticket.accountId]);

  // Required company assignment (tickets can't be worked without one)
  const [assignAccId, setAssignAccId] = useState('');
  const [newCompany, setNewCompany]   = useState('');
  const [assigningAcc, setAssigningAcc] = useState(false);
  const doAssignAccount = async (accountId) => {
    setAssigningAcc(true);
    try {
      await CRM.TicketsAPI.assignAccount(ticket.id, accountId);
      await refresh();
      showToast('Company assigned — contact created/linked from the requester email.', 'success');
    } catch (e) { showToast('Assign failed: ' + e.message, 'error'); }
    finally { setAssigningAcc(false); }
  };
  const createAndAssign = async () => {
    const name = newCompany.trim();
    if (!name) return showToast('Enter the new company name first.', 'error');
    setAssigningAcc(true);
    try {
      const acc = await CRM.AccountsAPI.create({ companyName: name });
      await CRM.TicketsAPI.assignAccount(ticket.id, acc.id);
      await refresh();
      showToast(`Created "${name}" and assigned the ticket.`, 'success');
    } catch (e) { showToast('Create failed: ' + e.message, 'error'); }
    finally { setAssigningAcc(false); }
  };

  const set = (k,v) => setForm(f=>({...f,[k]:v}));

  const save = async () => {
    setSaving(true);
    try {
      await CRM.TicketsAPI.update(ticket.id, { ...form,
        customFields: serializeTicketCustomFields(cfValues),
        ccTechIds: JSON.stringify(ccIds) });   // '[]' clears server-side
      await refresh();
      setEditing(false);
      showToast('Ticket updated!', 'success');
    } catch (e) {
      showToast('Failed to save: ' + e.message, 'error');
    } finally { setSaving(false); }
  };

  // One submit: posts the note/reply (if any) and applies the "Submit as"
  // status in the same action. Status-only submits are fine too.
  const submit = async () => {
    const text = commentText.trim();
    const statusChanged = submitStatus !== ticket.status;
    if (!text && !statusChanged) return;
    setSending(true);
    try {
      if (text) {
        if (composeMode === 'reply') await CRM.TicketsAPI.reply(ticket.id, text);
        else await CRM.TicketsAPI.addComment(ticket.id, text);
      }
      if (statusChanged) await CRM.TicketsAPI.updateStatus(ticket.id, submitStatus);
      setCommentText('');
      await refresh();
      await loadComments();
      const statusLabel = CRM.TICKET_STATUSES.find(s => s.id === submitStatus)?.label || submitStatus;
      showToast(
        text && statusChanged ? `${composeMode === 'reply' ? 'Reply sent' : 'Note added'} — status set to ${statusLabel}.`
        : text ? (composeMode === 'reply' ? 'Reply sent to customer.' : 'Note added.')
        : `Status set to ${statusLabel}.`, 'success');
    } catch (e) { showToast(e.message, 'error'); }
    finally { setSending(false); }
  };

  const onAccountChange = async (id) => {
    set('accountId', id);
    set('contactId', '');
    if (id) {
      try {
        const ctcts = await CRM.ContactsAPI.getByAccount(id);
        setEditContacts(ctcts);
      } catch { setEditContacts([]); }
    } else {
      setEditContacts([]);
    }
  };

  const techs = CRM.UsersAPI.getAll().filter(u => u.isActive !== false);   // any user is assignable

  // Use nested data from API includes for display
  const accountName = ticket.account?.companyName;
  const contactName = ticket.contact?.name;
  const techName    = ticket.assignedTech?.name || CRM.UsersAPI.getById(ticket.assignedTechId)?.name;
  const replyEmail  = ticket.requesterEmail || ticket.contact?.email || '';
  const canReply    = !!replyEmail;

  return (
    <div className="modal-overlay" {...overlayClose(onClose)}>
      <div className="modal-panel" style={{width:'min(760px,100%)'}} onClick={e=>e.stopPropagation()}>
        <div className="modal-hd">
          <div>
            <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:4}}>
              <span style={{fontSize:12,fontWeight:700,color:'var(--navy)',fontFamily:'monospace'}}>#{ticket.ticketNumber ?? ticket.id?.toString().slice(0,8)}</span>
              {ticket.channel === 'email' && <span className="badge badge-blue" style={{fontSize:10}}>✉ email</span>}
            </div>
            <div className="modal-title">{ticket.subject}</div>
          </div>
          <div style={{display:'flex',gap:8,alignItems:'center'}}>
            <span className={`badge ${STAT_BADGE[ticket.status]||'badge-gray'}`}>{ticket.status}</span>
            <span className={`badge ${PRI_BADGE[ticket.priority]||'badge-gray'}`}>{ticket.priority}</span>
            <button className="btn btn-sm" style={{background:'#FDF4FF',color:'#A21CAF',border:'1px solid #F5D0FE'}}
              onClick={() => setAnalyzing(true)}>✨ Analyze</button>
            <button className="modal-close" onClick={onClose}>×</button>
          </div>
        </div>

        {analyzing && <AnalyzeModal
          title={`Ticket Analysis — #${ticket.ticketNumber ?? ''}`}
          payload={{ type:'ticket', id: ticket.id }}
          onClose={() => setAnalyzing(false)}
        />}

        <div className="modal-body">
          {/* Required company assignment — tickets can't be worked without one */}
          {!ticket.accountId && (
            <div style={{border:'1.5px solid #F59E0B',background:'#FFFBEB',borderRadius:8,padding:'14px 16px',marginBottom:18}}>
              <div style={{fontSize:13,fontWeight:700,color:'#92400E',marginBottom:8}}>
                🏢 Assign this ticket to a company before working it
              </div>
              <div style={{display:'flex',gap:20,alignItems:'flex-start',flexWrap:'wrap'}}>
                {/* Existing account: picker with its Assign button directly below */}
                <div style={{minWidth:230,flex:1,display:'flex',flexDirection:'column',gap:8}}>
                  <div style={{fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em',color:'#92400E'}}>Existing account</div>
                  <SearchableSelect value={assignAccId} onChange={setAssignAccId}
                    options={accounts.map(a => ({ value: a.id, label: a.companyName }))}
                    emptyLabel="— Choose account —" />
                  <button className="btn btn-primary btn-sm" style={{alignSelf:'flex-start'}}
                    disabled={!assignAccId || assigningAcc}
                    onClick={() => doAssignAccount(assignAccId)}>{assigningAcc ? 'Assigning…' : 'Assign'}</button>
                </div>
                <div style={{alignSelf:'center',fontSize:12,fontWeight:700,color:'var(--g400)'}}>OR</div>
                {/* New account: name box with Create & assign directly below */}
                <div style={{minWidth:230,flex:1,display:'flex',flexDirection:'column',gap:8}}>
                  <div style={{fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em',color:'#92400E'}}>New account</div>
                  <input className="search-input" style={{width:'100%',minWidth:0}} placeholder="Enter company name"
                    value={newCompany} onChange={e => setNewCompany(e.target.value)} />
                  <button className="btn btn-ghost btn-sm" style={{alignSelf:'flex-start',border:'1px solid var(--g200)'}}
                    disabled={!newCompany.trim() || assigningAcc}
                    onClick={createAndAssign}>＋ Create & assign</button>
                </div>
              </div>
              {ticket.requesterEmail && (
                <div style={{fontSize:11,color:'#92400E',marginTop:6}}>
                  A contact for {ticket.requesterName || ticket.requesterEmail} ({ticket.requesterEmail}) is created under the company automatically.
                </div>
              )}
            </div>
          )}

          {/* Required custom fields — admin-flagged fields must be set before working */}
          {ticket.accountId && (() => {
            const savedVals = parseTicketCustomFields(ticket.customFields);
            const missing = customFieldDefs.filter(d => d.required && !(savedVals[d.key] || '').trim());
            if (missing.length === 0) return null;
            return (
              <div style={{border:'1.5px solid #F59E0B',background:'#FFFBEB',borderRadius:8,padding:'14px 16px',marginBottom:18}}>
                <div style={{fontSize:13,fontWeight:700,color:'#92400E',marginBottom:8}}>
                  🧩 Fill the required field{missing.length===1?'':'s'} before working this ticket
                </div>
                <div style={{display:'flex',gap:12,alignItems:'flex-end',flexWrap:'wrap'}}>
                  {missing.map(d => (
                    <div key={d.key} style={{minWidth:180,flex:1}}>
                      <div style={{fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'.05em',color:'#92400E',marginBottom:4}}>{d.label} *</div>
                      <SearchableSelect value={cfValues[d.key] || ''} onChange={v => setCfValues(x => ({ ...x, [d.key]: v }))}
                        options={d.options || []} emptyLabel="— Select —" />
                    </div>
                  ))}
                  <button className="btn btn-primary btn-sm" disabled={saving || missing.some(d => !(cfValues[d.key] || '').trim())}
                    onClick={save}>{saving ? 'Saving…' : 'Save fields'}</button>
                </div>
              </div>
            );
          })()}

          {/* Quick actions — status changes now ride on the composer's Submit */}
          {!editing && (
            <div style={{display:'flex',gap:6,marginBottom:20,flexWrap:'wrap'}}>
              <button className="btn btn-primary btn-sm" style={{marginLeft:'auto'}} onClick={()=>setEditing(true)}>✏️ Edit</button>
              {user.isAdmin && (
                <button className="btn btn-ghost btn-sm" style={{color:'#BC141E'}}
                  onClick={async () => {
                    if (!confirm(`Delete ticket #${ticket.ticketNumber ?? ''} permanently? This cannot be undone.`)) return;
                    try {
                      await CRM.TicketsAPI.delete(ticket.id);
                      showToast('Ticket deleted.', 'success');
                      onClose();
                    } catch (e) { showToast('Delete failed: ' + e.message, 'error'); }
                  }}>🗑 Delete</button>
              )}
            </div>
          )}

          {!editing ? (
            <div>
              {/* Info grid */}
              {(() => {
                // Inline quick-edit rows — same format as the Tech dropdown.
                // Status uses the guarded PATCH; the rest merge into a full PUT.
                const selCss = {flex:1,minWidth:0,fontSize:13,padding:'5px 8px'};
                const qRow = (label, control) => (
                  <div key={label} style={{display:'flex',alignItems:'center',gap:8,marginBottom:8}}>
                    <span style={{fontSize:12,color:'var(--g400)',minWidth:64}}>{label}</span>
                    {control}
                  </div>
                );
                const quickPatch = async (overrides, okMsg) => {
                  try {
                    await CRM.TicketsAPI.update(ticket.id, {
                      accountId: ticket.accountId, contactId: ticket.contactId,
                      subject: ticket.subject, description: ticket.description,
                      category: ticket.category, priority: ticket.priority, status: ticket.status,
                      assignedTechId: ticket.assignedTechId,
                      customFields: ticket.customFields || null,
                      ccTechIds: null,   // null = leave unchanged server-side
                      ...overrides,
                    });
                    await refresh();
                    showToast(okMsg || 'Saved.', 'success');
                  } catch (e) { showToast('Save failed: ' + e.message, 'error'); }
                };
                const quickStat = async (st) => {
                  try {
                    await CRM.TicketsAPI.updateStatus(ticket.id, st);
                    await refresh();
                    showToast('Status set to ' + (CRM.TICKET_STATUSES.find(x=>x.id===st)?.label || st) + '.', 'success');
                  } catch (e) { showToast(e.message, 'error'); }
                };
                const cfVals = parseTicketCustomFields(ticket.customFields);
                return (
              <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:20,marginBottom:20}}>
                <div>
                  <div className="form-section" style={{marginTop:0}}>Details</div>
                  <InfoRow label="Account" value={accountName} />
                  <InfoRow label="Contact" value={contactName} />
                  {ticket.channel === 'email' && (
                    <InfoRow label="Requester" value={`${ticket.requesterName || ''} <${ticket.requesterEmail || ''}>`.trim()} />
                  )}
                  {qRow('Category',
                    <select className="search-input" style={selCss} value={ticket.category || ''}
                      onChange={e => quickPatch({ category: e.target.value }, 'Category saved.')}>
                      {CRM.TICKET_CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
                    </select>)}
                  {customFieldDefs.map(d => qRow(d.label + (d.required ? ' *' : ''),
                    <select className="search-input" style={selCss} value={cfVals[d.key] || ''}
                      onChange={e => {
                        const v = { ...cfVals };
                        if (e.target.value) v[d.key] = e.target.value; else delete v[d.key];
                        quickPatch({ customFields: JSON.stringify(v) }, d.label + ' saved.');
                      }}>
                      <option value="">—</option>
                      {(d.options || []).map(o => <option key={o} value={o}>{o}</option>)}
                    </select>))}
                  <InfoRow label="Created" value={new Date(ticket.createdAt).toLocaleString()} />
                  {ticket.resolvedAt && <InfoRow label="Resolved" value={new Date(ticket.resolvedAt).toLocaleString()} />}
                </div>
                <div>
                  <div className="form-section" style={{marginTop:0}}>Assignment</div>
                  {qRow('Tech',
                    <select className="search-input" style={selCss} value={ticket.assignedTechId || ''}
                      onChange={async e => {
                        try {
                          await CRM.TicketsAPI.assign(ticket.id, e.target.value || null);
                          await refresh();
                          showToast(e.target.value ? 'Ticket reassigned.' : 'Ticket unassigned.', 'success');
                        } catch (err) { showToast('Reassign failed: ' + err.message, 'error'); }
                      }}>
                      <option value="">Unassigned</option>
                      {techs.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
                    </select>)}
                  {ccIds.length > 0 && (
                    <InfoRow label="CC" value={ccIds.map(id => CRM.UsersAPI.getById(id)?.name || '?').join(', ')} />
                  )}
                  {qRow('Priority',
                    <select className="search-input" style={selCss} value={ticket.priority || 'medium'}
                      onChange={e => quickPatch({ priority: e.target.value }, 'Priority saved.')}>
                      {CRM.TICKET_PRIORITIES.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
                    </select>)}
                  {qRow('Status',
                    <select className="search-input" style={selCss} value={ticket.status}
                      onChange={e => quickStat(e.target.value)}>
                      {CRM.TICKET_STATUSES.map(st => <option key={st.id} value={st.id}>{st.label}</option>)}
                    </select>)}
                </div>
              </div>
                );
              })()}
              <div style={{background:'var(--off)',borderRadius:6,padding:'12px 14px',fontSize:13,color:'var(--g600)',lineHeight:1.7,marginBottom:20}}>{ticket.description}</div>
            </div>
          ) : (
            <div>
              <div className="fg fg-2">
                <div className="field"><label>Status</label>
                  <select value={form.status} onChange={e=>set('status',e.target.value)}>
                    {CRM.TICKET_STATUSES.map(s=><option key={s.id} value={s.id}>{s.label}</option>)}
                  </select>
                </div>
                <div className="field"><label>Priority</label>
                  <select value={form.priority} onChange={e=>set('priority',e.target.value)}>
                    {CRM.TICKET_PRIORITIES.map(p=><option key={p.id} value={p.id}>{p.label}</option>)}
                  </select>
                </div>
              </div>
              <div className="fg fg-2">
                <div className="field"><label>Category</label>
                  <select value={form.category} onChange={e=>set('category',e.target.value)}>
                    {CRM.TICKET_CATEGORIES.map(c=><option key={c}>{c}</option>)}
                  </select>
                </div>
                <div className="field"><label>Assigned Tech</label>
                  <select value={form.assignedTechId||''} onChange={e=>set('assignedTechId',e.target.value)}>
                    <option value="">Unassigned</option>
                    {techs.map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
                  </select>
                </div>
              </div>
              <div className="fg fg-1">
                <div className="field"><label>CC Technicians</label>
                  <CcPicker techs={techs} value={ccIds} exclude={form.assignedTechId} onChange={setCcIds} />
                </div>
              </div>
              {customFieldDefs.length > 0 && (
                <div className="fg fg-2">
                  {customFieldDefs.map(d => (
                    <div className="field" key={d.key}><label>{d.label}{d.required ? ' *' : ''}</label>
                      <SearchableSelect value={cfValues[d.key] || ''} onChange={v => setCfValues(x => ({ ...x, [d.key]: v }))}
                        options={d.options || []} emptyLabel="— Select —" />
                    </div>
                  ))}
                </div>
              )}
              <div className="fg fg-2">
                <div className="field"><label>Account</label>
                  <SearchableSelect value={form.accountId||''} onChange={onAccountChange}
                    options={accounts.map(a => ({ value: a.id, label: a.companyName }))}
                    emptyLabel="— No Account —" />
                </div>
                <div className="field"><label>Contact</label>
                  <select value={form.contactId||''} onChange={e=>set('contactId',e.target.value)} disabled={!form.accountId}>
                    <option value="">— No Contact —</option>
                    {editContacts.map(c=><option key={c.id} value={c.id}>{c.name}{c.primary?' (Primary)':''}</option>)}
                  </select>
                </div>
              </div>
              <div className="fg fg-1"><div className="field"><label>Subject</label><input value={form.subject||''} onChange={e=>set('subject',e.target.value)} /></div></div>
              <div className="fg fg-1"><div className="field"><label>Description</label><textarea value={form.description||''} onChange={e=>set('description',e.target.value)} rows={4} /></div></div>
            </div>
          )}

          {/* Comments */}
          <div style={{borderTop:'1px solid var(--g100)',paddingTop:20}}>
            <div style={{fontWeight:700,fontSize:13,color:'var(--navy)',marginBottom:14}}>💬 Comments ({comments.length})</div>

            {/* Composer mode toggle: internal note vs email reply to the customer */}
            <div style={{display:'flex',gap:0,marginBottom:8}}>
              <button
                onClick={()=>setComposeMode('note')}
                style={{fontSize:12,fontWeight:600,padding:'5px 12px',cursor:'pointer',border:'1.5px solid var(--g200)',borderRight:'none',borderRadius:'6px 0 0 6px',
                  background:composeMode==='note'?'var(--navy)':'white',color:composeMode==='note'?'white':'var(--g600)'}}>
                🔒 Internal note
              </button>
              <button
                onClick={()=>{ if (canReply) setComposeMode('reply'); }}
                disabled={!canReply}
                title={canReply ? undefined : 'No customer email on this ticket'}
                style={{fontSize:12,fontWeight:600,padding:'5px 12px',cursor:canReply?'pointer':'not-allowed',border:'1.5px solid var(--g200)',borderRadius:'0 6px 6px 0',
                  background:composeMode==='reply'?'var(--blue)':'white',color:composeMode==='reply'?'white':(canReply?'var(--g600)':'var(--g400)'),opacity:canReply?1:.6}}>
                ✉ Reply to customer
              </button>
            </div>
            {composeMode==='reply' && (
              <div style={{fontSize:12,color:'var(--blue)',marginBottom:6}}>
                Sends as the support mailbox to <strong>{replyEmail}</strong>
              </div>
            )}
            <div style={{display:'flex',gap:8,marginBottom:16,alignItems:'flex-end'}}>
              <textarea
                style={{flex:1,border:`1.5px solid ${composeMode==='reply'?'var(--blue)':'var(--g200)'}`,borderRadius:6,padding:'9px 12px',fontSize:13,fontFamily:'var(--font)',color:'var(--navy)',resize:'none',height:60,outline:'none',background:composeMode==='reply'?'white':'var(--off)'}}
                placeholder={composeMode==='reply' ? 'Write your reply to the customer…' : 'Add a comment or update…'}
                value={commentText} onChange={e=>setCommentText(e.target.value)} />
              {/* Zendesk-style split submit: the note/reply posts AND the ticket
                  moves to the selected status in one action */}
              <div style={{display:'flex',flexShrink:0}}>
                <button className="btn btn-primary btn-sm"
                  disabled={sending || !ticket.accountId ||
                    customFieldDefs.some(d => d.required && !(parseTicketCustomFields(ticket.customFields)[d.key] || '').trim())}
                  style={{borderRadius:'6px 0 0 6px'}}
                  onClick={submit}
                  title={!ticket.accountId ? 'Assign a company first (banner above)'
                    : customFieldDefs.some(d => d.required && !(parseTicketCustomFields(ticket.customFields)[d.key] || '').trim())
                      ? 'Fill the required fields first (banner above)'
                    : (submitStatus !== ticket.status ? `Also sets status to ${CRM.TICKET_STATUSES.find(s=>s.id===submitStatus)?.label}` : undefined)}>
                  {sending ? 'Sending…'
                    : `${composeMode==='reply' ? 'Send' : 'Submit'} as ${CRM.TICKET_STATUSES.find(s=>s.id===submitStatus)?.label || submitStatus}`}
                </button>
                <select value={submitStatus} onChange={e=>setSubmitStatus(e.target.value)}
                  disabled={sending} aria-label="Submit as status"
                  style={{border:'none',borderLeft:'1px solid rgba(255,255,255,.35)',borderRadius:'0 6px 6px 0',
                    background:'var(--navy)',color:'white',fontSize:12,fontWeight:600,fontFamily:'var(--font)',
                    padding:'0 6px',cursor:'pointer',width:26,appearance:'none',WebkitAppearance:'none',
                    backgroundImage:'url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2710%27 height=%276%27%3E%3Cpath d=%27M0 0l5 6 5-6z%27 fill=%27white%27/%3E%3C/svg%3E")',
                    backgroundRepeat:'no-repeat',backgroundPosition:'center'}}>
                  {CRM.TICKET_STATUSES.map(s => <option key={s.id} value={s.id} style={{color:'var(--navy)',background:'white'}}>{s.label}</option>)}
                </select>
              </div>
            </div>
            <div style={{display:'flex',flexDirection:'column',gap:10}}>
              {comments.length===0 && <div style={{fontSize:13,color:'var(--g400)',textAlign:'center',padding:'16px'}}>No comments yet.</div>}
              {(() => {
                // Bounded render — imported threads can be huge. Comments are
                // oldest-first; keep the newest visible, expand upward.
                const all = comments.filter(c => !(c.direction === 'in' && c.content === '(original email)'));
                const hidden = Math.max(0, all.length - commentCap);
                const earlier = hidden > 0 ? (
                  <button key="__earlier" className="btn btn-ghost btn-sm" style={{alignSelf:'center'}}
                    onClick={() => setCommentCap(c => c + 100)}>Show earlier comments ({hidden} more)</button>
                ) : null;
                return [earlier, ...all.slice(hidden).map(c => {
                const dirBadge =
                  c.direction === 'in'  ? <span className="badge badge-blue"  style={{fontSize:10}}>📥 customer</span> :
                  c.direction === 'out' ? <span className="badge badge-green" style={{fontSize:10}}>📤 sent to customer</span> :
                                          <span className="badge badge-gray"  style={{fontSize:10}}>🔒 note</span>;
                const bg = c.direction === 'in' ? '#F0F7FF' : c.direction === 'out' ? '#F0FDF4' : 'var(--off)';
                return (
                <div key={c.id} style={{display:'flex',gap:10}}>
                  <div style={{width:32,height:32,borderRadius:'50%',background:'var(--navy)',color:'white',display:'flex',alignItems:'center',justifyContent:'center',fontSize:12,fontWeight:700,flexShrink:0}}>{c.userName.charAt(0)}</div>
                  <div style={{flex:1,background:bg,borderRadius:6,padding:'10px 14px'}}>
                    <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4,gap:8}}>
                      <span style={{fontSize:12,fontWeight:700,color:'var(--navy)',display:'flex',alignItems:'center',gap:6}}>{c.userName} {dirBadge}</span>
                      <span style={{fontSize:11,color:'var(--g400)'}}>{new Date(c.createdAt).toLocaleString()}</span>
                    </div>
                    <div style={{fontSize:13,color:'var(--g600)',lineHeight:1.6}}><NoteText text={c.content} limit={400} /></div>
                  </div>
                </div>
                );
              })];
              })()}
            </div>
          </div>
        </div>

        {editing && (
          <div className="modal-footer">
            <button className="btn btn-ghost" onClick={()=>{ setEditing(false); setForm({...ticket}); setCfValues(parseTicketCustomFields(ticket.customFields)); }}>Cancel</button>
            <button className="btn btn-primary" onClick={save} disabled={saving}>{saving ? 'Saving…' : 'Save Changes'}</button>
          </div>
        )}
      </div>
    </div>
  );
}

function AddTicketModal({ user, onClose, onSave, showToast }) {
  const techs = CRM.UsersAPI.getAll().filter(u => u.isActive !== false);   // any user is assignable
  const [accounts, setAccounts] = useState([]);
  const [contacts, setContacts] = useState([]);
  const [form, setForm] = useState({
    accountId:'', contactId:'', subject:'', description:'',
    category:'Technical', priority:'medium', status:'new',
    assignedTechId: user.id   // default: whoever creates it
  });
  const [saving, setSaving] = useState(false);
  const customFieldDefs = useCustomFieldDefs();
  const [cfValues, setCfValues] = useState({});
  const [ccIds, setCcIds] = useState([]);
  const set = (k,v) => setForm(f=>({...f,[k]:v}));

  useEffect(() => {
    CRM.AccountsAPI.getAll(user.role === 'rep' && !user.isAdmin ? { repId: user.id } : {}).then(setAccounts).catch(() => {});
  }, []);

  const onAccountChange = async (id) => {
    set('accountId', id);
    set('contactId', '');
    if (id) {
      try {
        const ctcts = await CRM.ContactsAPI.getByAccount(id);
        setContacts(ctcts);
      } catch { setContacts([]); }
    } else {
      setContacts([]);
    }
  };

  return (
    <div className="modal-overlay" {...overlayClose(onClose)}>
      <div className="modal-panel" onClick={e=>e.stopPropagation()}>
        <div className="modal-hd"><div className="modal-title">New Support Ticket</div><button className="modal-close" onClick={onClose}>×</button></div>
        <div className="modal-body">
          <div className="fg fg-2">
            <div className="field"><label>Account</label>
              <SearchableSelect value={form.accountId} onChange={onAccountChange}
                options={accounts.map(a => ({ value: a.id, label: a.companyName }))}
                emptyLabel="— Select Account —" />
            </div>
            <div className="field"><label>Contact</label>
              <select value={form.contactId} onChange={e=>set('contactId',e.target.value)} disabled={!form.accountId}>
                <option value="">— Select Contact —</option>
                {contacts.map(c=><option key={c.id} value={c.id}>{c.name}{c.primary?' (Primary)':''}</option>)}
              </select>
            </div>
          </div>
          <div className="fg fg-1"><div className="field"><label>Subject *</label><input value={form.subject} onChange={e=>set('subject',e.target.value)} placeholder="Brief description of the issue" /></div></div>
          <div className="fg fg-1"><div className="field"><label>Description</label><textarea value={form.description} onChange={e=>set('description',e.target.value)} placeholder="Full details…" rows={4} /></div></div>
          <div className="fg fg-3">
            <div className="field"><label>Category</label>
              <select value={form.category} onChange={e=>set('category',e.target.value)}>
                {CRM.TICKET_CATEGORIES.map(c=><option key={c}>{c}</option>)}
              </select>
            </div>
            <div className="field"><label>Priority</label>
              <select value={form.priority} onChange={e=>set('priority',e.target.value)}>
                {CRM.TICKET_PRIORITIES.map(p=><option key={p.id} value={p.id}>{p.label}</option>)}
              </select>
            </div>
            <div className="field"><label>Assign To</label>
              <select value={form.assignedTechId} onChange={e=>set('assignedTechId',e.target.value)}>
                <option value="">Unassigned</option>
                {techs.map(t=><option key={t.id} value={t.id}>{t.name}</option>)}
              </select>
            </div>
          </div>
          <div className="fg fg-1">
            <div className="field"><label>CC Technicians</label>
              <CcPicker techs={techs} value={ccIds} exclude={form.assignedTechId} onChange={setCcIds} />
            </div>
          </div>
          {customFieldDefs.length > 0 && (
            <div className="fg fg-2">
              {customFieldDefs.map(d => (
                <div className="field" key={d.key}><label>{d.label}{d.required ? ' *' : ''}</label>
                  <SearchableSelect value={cfValues[d.key] || ''} onChange={v => setCfValues(x => ({ ...x, [d.key]: v }))}
                    options={d.options || []} emptyLabel="— Select —" />
                </div>
              ))}
            </div>
          )}
        </div>
        <div className="modal-footer">
          <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" disabled={saving}
            onClick={async () => {
              if (!form.subject) return;
              const missingReq = customFieldDefs.filter(d => d.required && !(cfValues[d.key] || '').trim());
              if (missingReq.length) return showToast('Required field' + (missingReq.length===1?'':'s') + ': ' + missingReq.map(d=>d.label).join(', '), 'error');
              setSaving(true);
              try { await onSave({ ...form,
                customFields: serializeTicketCustomFields(cfValues),
                ccTechIds: ccIds.length ? JSON.stringify(ccIds) : null }); }
              finally { setSaving(false); }
            }}>{saving ? 'Creating…' : 'Create Ticket'}</button>
        </div>
      </div>
    </div>
  );
}

// ── COMPANY LINKER (admin) ──
// Attaches unlinked tickets to accounts by requester email domain. A domain is
// unique to a company, so this reuses an account that already owns the domain
// (no duplicate) or creates one for a company that's only in Zendesk so far.
function CompanyLinkerModal({ showToast, onClose, onChanged }) {
  const [report, setReport] = useState(null);
  const [accounts, setAccounts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [busy, setBusy] = useState('');        // domain currently being linked
  const [done, setDone] = useState({});        // domain -> result summary
  const [names, setNames] = useState({});      // domain -> edited company name (new-account path)
  const [picks, setPicks] = useState({});      // domain -> chosen existing accountId
  const [changed, setChanged] = useState(false);

  const load = () => {
    setLoading(true);
    Promise.all([
      CRM.TicketsAPI.linkingReport(),
      CRM.AccountsAPI.getAll().catch(() => []),
    ])
      .then(([rep, accs]) => { setReport(rep); setAccounts(accs); })
      .catch(e => showToast('Failed to load report: ' + e.message, 'error'))
      .finally(() => setLoading(false));
  };
  useEffect(load, []);

  const accountOptions = accounts
    .map(a => ({ value: a.id, label: a.companyName }))
    .sort((a, b) => (a.label || '').localeCompare(b.label || ''));

  const linkOne = async (d) => {
    setBusy(d.domain);
    try {
      const r = await CRM.TicketsAPI.linkDomain(d.domain, names[d.domain], picks[d.domain] || null);
      setDone(prev => ({ ...prev, [d.domain]: r }));
      setChanged(true);
    } catch (e) {
      showToast('Link failed: ' + e.message, 'error');
    } finally { setBusy(''); }
  };

  const pending = (report?.domains || []).filter(d => !done[d.domain]);

  const linkAllShown = async () => {
    if (!confirm(`Link all ${pending.length} companies shown? Rows with a chosen account link to it; the rest get a new Inactive account for review.`)) return;
    for (const d of pending) { /* sequential to keep it gentle */ await linkOne(d); }
    showToast('Linking complete.', 'success');
  };

  const close = () => { onClose(); if (changed) onChanged(); };

  return (
    <div className="modal-overlay" onClick={close}>
      <div className="modal-panel" style={{width:'min(820px,100%)'}} onClick={e => e.stopPropagation()}>
        <div className="modal-hd">
          <div>
            <div className="modal-title">Link companies</div>
            <div style={{fontSize:12,color:'var(--g600)',marginTop:2}}>
              Attach unlinked tickets to accounts by email domain. Pick an existing account per row, or leave it blank to create a new one (added Inactive for review).
            </div>
          </div>
          <button className="modal-close" onClick={close}>×</button>
        </div>

        <div className="modal-body">
          {loading && <div style={{padding:24,textAlign:'center',color:'var(--g400)'}}>Analyzing tickets…</div>}
          {!loading && report && (
            <React.Fragment>
              <div style={{display:'flex',gap:16,flexWrap:'wrap',marginBottom:16,fontSize:13}}>
                <span><strong style={{color:'var(--navy)'}}>{report.linked}</strong> linked</span>
                <span><strong style={{color:'#BC141E'}}>{report.unlinked}</strong> unlinked</span>
                <span style={{color:'var(--g600)'}}>{report.distinctDomains} companies · {report.unlinkableFreeMail} from personal email (can't link)</span>
              </div>

              {pending.length === 0 ? (
                <div style={{padding:20,textAlign:'center',color:'var(--g400)',fontSize:13}}>
                  {report.domains.length === 0 ? 'No unlinked business-email tickets. 🎉' : 'All shown companies linked.'}
                </div>
              ) : (
                <React.Fragment>
                  <div style={{display:'flex',justifyContent:'flex-end',marginBottom:8}}>
                    <button className="btn btn-ghost btn-sm" onClick={linkAllShown} disabled={!!busy}>Link all ({pending.length})</button>
                  </div>
                  <table className="tbl" style={{margin:0}}>
                    <thead><tr>
                      <th>Domain</th><th style={{width:60}}>Tickets</th>
                      <th style={{minWidth:190}}>Link to existing account</th>
                      <th style={{minWidth:150}}>…or create as</th>
                      <th style={{width:110}}></th>
                    </tr></thead>
                    <tbody>
                      {report.domains.map(d => {
                        const res = done[d.domain];
                        const picked = picks[d.domain];
                        if (res) return (
                          <tr key={d.domain}>
                            <td style={{fontFamily:'monospace',fontSize:12}}>{d.domain}</td>
                            <td style={{fontWeight:700,color:'var(--navy)'}}>{d.count}</td>
                            <td colSpan={3}><span style={{fontSize:12,color:'var(--green)'}}>
                              ✓ {res.ticketsLinked} ticket{res.ticketsLinked===1?'':'s'} linked{res.created ? ' · new account created' : ' · existing account'}
                            </span></td>
                          </tr>
                        );
                        return (
                          <tr key={d.domain}>
                            <td style={{fontFamily:'monospace',fontSize:12}}>{d.domain}</td>
                            <td style={{fontWeight:700,color:'var(--navy)'}}>{d.count}</td>
                            <td>
                              {d.existingAccount
                                ? <span style={{fontSize:12,color:'var(--g600)'}}>Auto — an account already owns this domain</span>
                                : <SearchableSelect value={picked || ''} onChange={v => setPicks(p => ({ ...p, [d.domain]: v }))}
                                    options={accountOptions} emptyLabel="— New account —" />}
                            </td>
                            <td>
                              <input className="search-input" style={{width:'100%',minWidth:0,fontSize:12,padding:'4px 8px'}}
                                placeholder={titleize(d.domain)}
                                value={names[d.domain] ?? ''} disabled={d.existingAccount || !!picked}
                                onChange={e => setNames(n => ({ ...n, [d.domain]: e.target.value }))} />
                            </td>
                            <td>
                              <button className="btn btn-ghost btn-xs" disabled={busy===d.domain || !!busy}
                                onClick={() => linkOne(d)}>
                                {busy===d.domain ? 'Linking…' : (d.existingAccount || picked ? 'Link' : 'Create & link')}
                              </button>
                            </td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </React.Fragment>
              )}
            </React.Fragment>
          )}
        </div>
        <div className="modal-footer">
          <button className="btn btn-primary" onClick={close}>Done</button>
        </div>
      </div>
    </div>
  );
}

function titleize(domain) {
  const label = (domain || '').split('.')[0].replace(/[-_]/g, ' ');
  return label.replace(/\b\w/g, c => c.toUpperCase());
}

Object.assign(window, { Tickets, TicketDetailModal, AddTicketModal, CompanyLinkerModal });
