/* NUtec Planning v2 — Go slice front end.
 * Plan board fed by live nutec_factory data via the Go API.
 * Visual language follows the Claude Design concept (plan board, phase track). */
const { useState, useEffect, useMemo } = React;

const PHASES = ['Captured', 'Blending', 'Filtration', 'Filling', 'Packaging', 'FM Store'];
const PHASE_COLOR = {
  'Captured':   'var(--nu-grey-500)',
  'Blending':   'var(--gem-sapphire)',
  'Filtration': 'var(--gem-aquamarine)',
  'Filling':    'var(--brand)',
  'Packaging':  'var(--gem-quartz)',
  'FM Store':   'var(--gem-emerald)',
  'FX store':   'var(--prod-nucoat)',
};
const FAMILY_COLOR = {
  'Solvent':        'var(--gem-topaz)',
  'UV':             'var(--gem-amethyst)',
  'Water Based':    'var(--gem-aquamarine)',
  'Coatings':       'var(--prod-nucoat)',
  'Chips':          'var(--gem-granite)',
  'Hand Sanitiser': 'var(--nu-grey-500)',
};
function famColor(f) { return FAMILY_COLOR[f] || 'var(--nu-grey-500)'; }

function fmtDate(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  return d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' });
}
function daysFromToday(iso) {
  if (!iso) return null;
  return Math.round((new Date(iso) - new Date().setHours(0, 0, 0, 0)) / 86400000);
}

function Check() {
  return (
    <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round">
      <polyline points="20 6 9 17 4 12" />
    </svg>
  );
}

/* ---- Phase track: nodes with fill advancing to the reached phase ---- */
function Track({ names, idx, done }) {
  const n = names.length;
  const eff = done ? n - 1 : idx;
  const color = PHASE_COLOR[names[Math.min(eff, n - 1)]] || 'var(--brand)';
  return (
    <div className="track" role="img" aria-label={done ? 'Complete' : `Reached ${names[eff]}`}>
      <div className="track-line" />
      <div className="track-fill" style={{ width: `calc((100% - 10px) * ${eff / (n - 1)})`, background: color }} />
      {names.map((p, i) => {
        const reached = i <= eff;
        return (
          <div key={p}
            className={'node' + (reached ? ' reached' : '') + (i === eff && !done ? ' lead' : '')}
            style={{ left: `calc(5px + (100% - 10px) * ${i / (n - 1)})`, color: reached ? color : undefined }}
            title={p}>
            {reached && <Check />}
          </div>
        );
      })}
    </div>
  );
}

function PhaseTrack({ phaseIdx }) {
  return <Track names={PHASES} idx={phaseIdx} done={false} />;
}

/* Blend jobs only ever traverse Captured → Blending → Filtration; their liquid
 * then moves on inside the F job. A three-node track keeps that honest. */
const BLEND_PHASES = ['Captured', 'Blending', 'Filtration'];

function blendDone(j) { return j.rtStatus === 'Complete' || !!j.completedAt; }

function BlendTrack({ j }) {
  return (
    <div style={{ maxWidth: 150 }}>
      <Track names={BLEND_PHASES} idx={Math.min(j.phaseIdx, BLEND_PHASES.length - 1)} done={blendDone(j)} />
    </div>
  );
}

function blendStatusText(j) {
  if (blendDone(j)) return 'complete' + (j.completedAt ? ` ${fmtWhen(j.completedAt)}` : '');
  if (j.startedAt) return `${j.phase} since ${fmtWhen(j.startedAt)}`;
  return 'not started';
}

/* A solo fill/pack job's own journey has no blending — those phases belong to
 * its (completed or never-created) blend job. FX jobs end in the cold store. */
const FILL_PHASES = ['Captured', 'Filling', 'Packaging', 'FM Store'];
const FX_PHASES = ['Captured', 'Filling', 'FX store'];

function SoloTrack({ j }) {
  if (j.jobKind === 'Blend') {
    return <div style={{ maxWidth: 150 }}><Track names={BLEND_PHASES} idx={Math.min(j.phaseIdx, BLEND_PHASES.length - 1)} done={blendDone(j)} /></div>;
  }
  if (j.fx) {
    const idx = j.phaseIdx >= PHASE_ORDER_FILLING ? 1 : 0;
    return <div style={{ maxWidth: 150 }}><Track names={FX_PHASES} idx={idx} done={blendDone(j)} /></div>;
  }
  if (j.jobKind === 'FillPack' && FILL_PHASES.includes(j.phase)) {
    return <div style={{ maxWidth: 220 }}><Track names={FILL_PHASES} idx={FILL_PHASES.indexOf(j.phase)} done={false} /></div>;
  }
  return <PhaseTrack phaseIdx={j.phaseIdx} />;
}
const PHASE_ORDER_FILLING = 3;

function DueCell({ iso, muted }) {
  const d = daysFromToday(iso);
  return (
    <div>
      <span className="due-d">{fmtDate(iso)}</span>
      {d != null && (
        <span className={'due-in' + (muted ? '' : d <= 0 ? ' bad' : d <= 3 ? ' warn' : '')}>
          {d < 0 ? `${-d}d overdue` : d === 0 ? 'due today' : `in ${d}d`}
        </span>
      )}
    </div>
  );
}

function IntermediateTag() {
  return <span className="int-tag" title="Governed rule: intermediate blend job — counted for capacity, excluded from order linkage and SLA colouring">intermediate</span>;
}

function SampleTag() {
  return <span className="int-tag" title="Retained-sample job (S-suffix) — off the plan by default, serves no customer demand">sample</span>;
}

function FxTag() {
  return <span className="fx-tag" title="FX cold-store fill: bulk goes to cold storage; it returns to packaging later under a fresh month-aligned job/lot for shelf life">FX · cold store</span>;
}

function DraftTag() {
  return <span className="draft-tag" title="Draft job planned in the platform — not yet confirmed in SysPro">draft</span>;
}

const SOURCE_TITLE = {
  allocated: 'A stock-lot allocation names this job for the order line',
  planned: 'Explicitly linked when the job was planned',
  inferred: 'Same F-code as open order lines — not an explicit link',
};

function DemandText({ demand }) {
  if (!demand) return <span style={{ color: 'var(--fg-subtle)' }}>(stock production)</span>;
  const approx = demand.source === 'inferred';
  return (
    <span title={SOURCE_TITLE[demand.source]}>
      {demand.customer
        ? <>{approx ? '≈ ' : ''}{demand.customer}{demand.orderNo ? ` · ${demand.orderNo.replace(/^0+/, '')}` : demand.lines > 1 ? ` · ${demand.lines} lines` : ''}</>
        : <>{approx ? '≈ ' : ''}{demand.lines} open line{demand.lines > 1 ? 's' : ''} · {demand.customers} customers</>}
    </span>
  );
}

/* ---- Job detail drawer ---- */
function fmtWhen(iso) {
  if (!iso) return null;
  const d = new Date(iso);
  return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) + ' ' +
    d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
}

function Drawer({ jobNo, onClose, onSelect }) {
  const [detail, setDetail] = useState(null);
  const [err, setErr] = useState(null);

  useEffect(() => {
    if (!jobNo) return;
    setDetail(null); setErr(null);
    fetch(`/api/plan/jobs/${encodeURIComponent(jobNo)}`)
      .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(setDetail)
      .catch((e) => setErr(String(e)));
  }, [jobNo]);

  useEffect(() => {
    function k(e) { if (e.key === 'Escape') onClose(); }
    window.addEventListener('keydown', k);
    return () => window.removeEventListener('keydown', k);
  }, [onClose]);

  const open = !!jobNo;
  const d = detail;
  const curIdx = d ? d.phaseIdx : -1;
  return (
    <>
      <div className={'scrim' + (open ? ' open' : '')} onClick={onClose} />
      <aside className={'drawer' + (open ? ' open' : '')} aria-hidden={!open}>
        {open && (
          <>
            <div className="drawer-h">
              <button className="close" onClick={onClose} aria-label="Close">✕</button>
              {err && <div className="error">Failed to load {jobNo}: {err}</div>}
              {!d && !err && <div className="loading">Loading {jobNo}…</div>}
              {d && (
                <>
                  <div className="eyerow">
                    <span className="fam-dot" style={{ background: famColor(d.family), width: 18, height: 18, borderRadius: 5, fontSize: 10 }}>{(d.family || '?')[0]}</span>
                    <span className="jno">{d.job}</span>
                    <span style={{ fontSize: 11, color: 'var(--fg-subtle)' }}>{d.jobKind === 'Blend' ? 'blend job' : d.jobKind === 'FillPack' ? 'fill/pack job' : ''}</span>
                    {d.intermediate && <IntermediateTag />}
                  </div>
                  <h2>{d.stockCode}</h2>
                  <div className="fam">{d.description || 'no item master row'}{d.family ? ` · ${d.family}` : ''}{d.colour && d.colour !== 'N/A' ? ` · ${d.colour}` : ''}</div>
                  <div style={{ marginTop: 10 }} className="phase-badge">
                    <span className="sw" style={{ background: PHASE_COLOR[d.phase] }} />{d.phase}
                    <span style={{ fontWeight: 400, color: 'var(--fg-subtle)', marginLeft: 6 }}>{d.planStatus}{d.rtStatus ? ` · riteTIME ${d.rtStatus}` : ' · no scans yet'}</span>
                  </div>
                </>
              )}
            </div>
            {d && (
              <div className="drawer-body">
                <div className="drawer-sec">
                  <h4>Job details</h4>
                  <dl className="kv">
                    <dt>Stock code</dt><dd className="mono">{d.stockCode}</dd>
                    <dt>Planned qty</dt><dd>{Number(d.qty).toLocaleString()} {d.jobKind === 'Blend' ? 'kg' : 'units'}</dd>
                    {d.packType && <><dt>Packaging</dt><dd>{d.packType}</dd></>}
                    <dt>Job start</dt><dd>{fmtDate(d.jobStart)}</dd>
                    <dt>Delivery</dt><dd>{fmtDate(d.jobDelivery)}</dd>
                    {d.workCentre && <><dt>Work centre</dt><dd>{d.workCentre}</dd></>}
                    {d.lastOp && <><dt>Last scan</dt><dd className="mono">op {d.lastOp}</dd></>}
                    {d.receiptedQty > 0 && <><dt>FM receipted</dt><dd>{Number(d.receiptedQty).toLocaleString()} units</dd></>}
                  </dl>
                </div>

                <div className="drawer-sec">
                  <h4>Milestones</h4>
                  <div className="mile">
                    {(d.milestones || []).map((m, i) => {
                      const done = !!m.reachedAt && i < curIdx;
                      const now = i === curIdx;
                      return (
                        <div key={m.phase} className={'mile-item' + (done ? ' done' : '') + (now ? ' now' : '')}>
                          {m.reachedAt ? (done ? 'Reached' : now ? 'Current' : 'Seen') : (now ? 'Current' : 'Pending')} · {m.phase}
                          {m.reachedAt && <span className="mile-when">{fmtWhen(m.reachedAt)}{m.workCentre ? ` · ${m.workCentre}` : ''}</span>}
                        </div>
                      );
                    })}
                  </div>
                </div>

                {(d.related || []).length > 0 && (
                  <div className="drawer-sec">
                    <h4>Related jobs · base {d.baseJob}</h4>
                    {d.related.map((r) => (
                      <div key={r.job} className="linkrow" onClick={() => onSelect(r.job)}>
                        <span className="sw" style={{ width: 9, height: 9, borderRadius: 2, background: PHASE_COLOR[r.phase], display: 'inline-block' }} />
                        <span className="lr-id">{r.job}</span>
                        <span style={{ color: 'var(--fg-muted)' }}>{r.stockCode}</span>
                        <span className="lr-sub">{r.phase}{r.planStatus === 'Complete' ? ' · complete' : ''}</span>
                      </div>
                    ))}
                  </div>
                )}

                {(d.receipts || []).length > 0 && (
                  <div className="drawer-sec">
                    <h4>FM store receipts</h4>
                    {d.receipts.map((r, i) => (
                      <div key={i} className={'rcpt' + (r.isDisposal ? ' disposal' : '')}>
                        <span>{fmtWhen(r.at)}{r.bin ? ` · bin ${r.bin}` : ''}{r.isDisposal ? ' · disposal' : ''}</span>
                        <span className="r-qty">{Number(r.qty).toLocaleString()}</span>
                      </div>
                    ))}
                  </div>
                )}

                {(d.orderLines || []).length > 0 && (
                  <div className="drawer-sec">
                    <h4>Linked sales orders</h4>
                    {d.orderLines.map((ol, i) => (
                      <div key={i} className="linkrow" style={{ cursor: 'default' }} title={SOURCE_TITLE[ol.source]}>
                        <span className="lr-id">{ol.source === 'inferred' ? '≈ ' : ''}{ol.orderNo.replace(/^0+/, '')}</span>
                        <span style={{ color: 'var(--fg-muted)' }}>{ol.customer}</span>
                        <span className={'src-tag src-' + ol.source}>{ol.source}</span>
                        <span className="lr-sub">{Number(ol.qty).toLocaleString()} u · {fmtDate(ol.requestedDate)}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            )}
          </>
        )}
      </aside>
    </>
  );
}

/* ---- Jobs board: rows grouped by base job (blend M job + its F products) ---- */
function groupJobs(jobs) {
  const map = new Map();
  for (const j of jobs) {
    const key = j.baseJob || j.job;
    if (!map.has(key)) map.set(key, []);
    map.get(key).push(j);
  }
  const groups = [...map.entries()].map(([base, members]) => {
    // Several blend jobs per base are sequential batches of one liquid
    // (e.g. KX2 inks: M1 + M2 at 250 kg each form a 500 kg batch that is
    // bottled by a single F job). Sort by job number to keep the sequence.
    const blends = members.filter((m) => m.jobKind === 'Blend').sort((a, b) => a.job < b.job ? -1 : 1);
    const products = members.filter((m) => m.jobKind !== 'Blend');
    const blendKg = blends.reduce((s, m) => s + m.qty, 0);
    const lead = members.reduce((a, b) => (b.phaseIdx > a.phaseIdx ? b : a));
    const due = members.map((m) => m.jobDelivery).filter(Boolean).sort()[0] || null;
    const receipted = members.reduce((s, m) => s + m.receiptedQty, 0);
    const head = blends[0] || members[0];
    return { base, members, blends, products, blendKg, lead, due, receipted, head };
  });
  groups.sort((a, b) => (a.due || '9999') < (b.due || '9999') ? -1 : 1);
  return groups;
}

function JobIdCell({ j, showKind = true }) {
  return (
    <div style={{ minWidth: 0 }}>
      <div><span className="jno">{j.job}</span>{' '}
        {showKind && <span style={{ fontSize: 11, color: 'var(--fg-subtle)' }}>{j.jobKind === 'Blend' ? 'blend' : j.jobKind === 'FillPack' ? 'fill/pack' : ''}</span>}
        {j.intermediate && <IntermediateTag />}
        {j.sample && <SampleTag />}
        {j.fx && <FxTag />}
        {j.planStatus === 'Draft' && <DraftTag />}
      </div>
      <div className="jmeta"><b>{j.stockCode}</b> · {j.description || 'no item master row'}{j.colour && j.colour !== 'N/A' ? ` · ${j.colour}` : ''}</div>
      {!j.intermediate && !j.sample && <div className="jmeta"><DemandText demand={j.demand} /></div>}
    </div>
  );
}

function ReceiptChip({ qty }) {
  return qty > 0
    ? <span className="stock-chip">{Number(qty).toLocaleString()} receipted</span>
    : <span className="stock-chip none">—</span>;
}

function PhaseBadge({ phase }) {
  return <div className="phase-badge"><span className="sw" style={{ background: PHASE_COLOR[phase] }} />{phase}</div>;
}

/* A base with only one open job renders as a plain row. */
function SingleJobRow({ j, onSelect }) {
  return (
    <div className="row job" style={{ cursor: 'pointer' }} onClick={() => onSelect(j.job)}>
      <div className="jobid">
        <span className="fam-dot" style={{ background: famColor(j.family) }}>{(j.family || '?')[0]}</span>
        <JobIdCell j={j} />
      </div>
      <div>
        <SoloTrack j={j} />
        <div className="jmeta" style={{ marginTop: 4 }}>
          {Number(j.qty).toLocaleString()} {j.jobKind === 'Blend' ? 'kg' : 'units'} planned
          {j.workCentre ? <> · at <b>{j.workCentre}</b></> : null}
          {j.lastOp ? <> · last scan {j.lastOp}</> : null}
          {j.fx ? <> · fills to the FX cold store</>
            : j.jobKind === 'FillPack' && !j.sample ? <> · no open blend job — from stock or a completed blend</> : null}
        </div>
      </div>
      <div><ReceiptChip qty={j.receiptedQty} /></div>
      <DueCell iso={j.jobDelivery} muted={j.intermediate} />
      {(j.jobKind === 'Blend' || j.fx) && blendDone(j)
        ? <div className="phase-badge"><span className="sw" style={{ background: 'var(--success)' }} />Complete</div>
        : <PhaseBadge phase={j.phase} />}
    </div>
  );
}

/* Blend job + its fill/pack products as one group: the header carries the
 * unified phase track (blend phases come from the M job, later phases from
 * the leading product); product sub-rows show their own state without a
 * misleading blending segment of their own. */
function GroupSubRow({ j, label, unit, onSelect }) {
  return (
    <div className="row job sub" style={{ cursor: 'pointer' }} onClick={() => onSelect(j.job)}>
      <div className="jobid sub-indent">
        {label
          ? <div style={{ minWidth: 0 }}>
              <div><span className="jno">{j.job}</span> <span className="batch-tag">{label}</span>{j.intermediate && <IntermediateTag />}{j.sample && <SampleTag />}</div>
              <div className="jmeta"><b>{j.stockCode}</b> · {j.description || 'no item master row'}</div>
            </div>
          : <JobIdCell j={j} showKind={false} />}
      </div>
      {unit === 'kg'
        ? <div>
            <BlendTrack j={j} />
            <div className="jmeta" style={{ marginTop: 3 }}>
              {Number(j.qty).toLocaleString()} kg · {blendStatusText(j)}
              {!blendDone(j) && j.workCentre ? <> · at <b>{j.workCentre}</b></> : null}
            </div>
          </div>
        : <div className="jmeta">
            {Number(j.qty).toLocaleString()} {unit}
            {j.workCentre ? <> · at <b>{j.workCentre}</b></> : null}
          </div>}
      <div><ReceiptChip qty={j.receiptedQty} /></div>
      <DueCell iso={j.jobDelivery} muted={j.intermediate} />
      {unit === 'kg' && blendDone(j)
        ? <div className="phase-badge"><span className="sw" style={{ background: 'var(--success)' }} />Complete</div>
        : <PhaseBadge phase={j.phase} />}
    </div>
  );
}

function GroupRows({ g, onSelect }) {
  const head = g.head;
  const oneBlend = g.blends.length === 1 ? g.blends[0] : null;
  const seq = g.blends.length > 1;
  return (
    <>
      <div className="row job group-h" style={{ cursor: 'pointer' }} onClick={() => onSelect(head.job)}>
        <div className="jobid">
          <span className="fam-dot" style={{ background: famColor(head.family) }}>{(head.family || '?')[0]}</span>
          <div style={{ minWidth: 0 }}>
            <div>
              <span className="jno">{oneBlend ? oneBlend.job : g.base}</span>{' '}
              <span style={{ fontSize: 11, color: 'var(--fg-subtle)' }}>
                {seq ? `${g.blends.length} blend batches` : oneBlend ? 'blend' : 'group'} · {g.products.length} product{g.products.length !== 1 ? 's' : ''}
              </span>
              {head.intermediate && <IntermediateTag />}
            </div>
            {g.blends.length > 0
              ? <div className="jmeta"><b>{g.blends[0].stockCode}</b> · {g.blends[0].description || 'no item master row'}</div>
              : <div className="jmeta">blend job complete or not on the floor</div>}
          </div>
        </div>
        <div>
          <PhaseTrack phaseIdx={g.lead.phaseIdx} />
          <div className="jmeta" style={{ marginTop: 4 }}>
            {g.blendKg > 0 ? <>{Number(g.blendKg).toLocaleString()} kg blend{seq ? ' in sequence' : ''}</> : null}
            {seq && <> · {g.blends.map((b, i) => (
              <span key={b.job}>{i > 0 && ' — '}batch {i + 1} <b>{blendDone(b) ? 'complete' : b.startedAt ? b.phase.toLowerCase() : 'not started'}</b></span>
            ))}</>}
            {oneBlend && oneBlend.workCentre ? <> · at <b>{oneBlend.workCentre}</b></> : null}
            {g.blends.length === 0 && g.lead.workCentre ? <>at <b>{g.lead.workCentre}</b></> : null}
          </div>
        </div>
        <div><ReceiptChip qty={g.receipted} /></div>
        <DueCell iso={g.due} muted={head.intermediate} />
        <PhaseBadge phase={g.lead.phase} />
      </div>
      {seq && g.blends.map((b, i) => (
        <GroupSubRow key={b.job} j={b} label={`batch ${i + 1} of ${g.blends.length}`} unit="kg" onSelect={onSelect} />
      ))}
      {g.products.map((p) => (
        <GroupSubRow key={p.job} j={p} unit="units" onSelect={onSelect} />
      ))}
    </>
  );
}

function JobsBoard({ groups, onSelect }) {
  return (
    <div className="board">
      <div className="row head">
        <div>Job</div><div>Production progress</div><div>FM store</div><div>Delivery</div><div>Phase</div>
      </div>
      {groups.map((g) => g.members.length === 1
        ? <SingleJobRow key={g.base} j={g.members[0]} onSelect={onSelect} />
        : <GroupRows key={g.base} g={g} onSelect={onSelect} />)}
      {groups.length === 0 && <div className="empty">No jobs match your filters.</div>}
    </div>
  );
}

/* ---- Orders: shared line renderer (plan "by sales order" view + sales page) ---- */
function lineHeld(l) { return l.stockOnHold === 'P' || l.stockOnHold === 'F'; }

function HoldTag({ code }) {
  return (
    <span className="hold-tag" title={`Item master stock_on_hold = ${code} — this item is on stock hold, yet the order line is open`}>
      stock hold · {code}
    </span>
  );
}

function OrderLineRow({ l, onSelectJob }) {
  const jobs = l.jobs || [];
  const best = jobs.reduce((a, b) => (b.phaseIdx > (a ? a.phaseIdx : -1) ? b : a), null);
  return (
    <div className={'oline' + (lineHeld(l) ? ' held' : '')}>
      <div>
        <span className="fam-dot" style={{ background: famColor(l.family), width: 18, height: 18, borderRadius: 5, fontSize: 10, display: 'inline-flex', verticalAlign: '-4px', marginRight: 8 }}>{(l.family || '?')[0]}</span>
        <b>{l.fCode}</b> <span style={{ color: 'var(--fg-muted)' }}>{l.description}</span>
        {lineHeld(l) && <HoldTag code={l.stockOnHold} />}
      </div>
      <div className="qty">{Number(l.qty).toLocaleString()}</div>
      <div>
        {best
          ? <>
              <PhaseTrack phaseIdx={best.phaseIdx} />
              <div className="jmeta" style={{ marginTop: 3 }}>
                {jobs.map((jr) => (
                  <span key={jr.job} className="job-chip" title={`${jr.phase} · ${SOURCE_TITLE[jr.source]}`}
                    onClick={(e) => { e.stopPropagation(); onSelectJob(jr.job); }}>
                    {jr.source === 'inferred' ? '≈ ' : ''}{jr.job}
                  </span>
                ))}
              </div>
            </>
          : <span style={{ color: 'var(--fg-subtle)', fontSize: 12 }}>no jobs on the floor</span>}
      </div>
      <div>
        {l.allocatedQty > 0
          ? <span className="stock-chip" title={(l.lots || []).map((x) => `${x.lot} (${x.qtyAllocated})`).join(', ')}>{l.allocatedQty.toLocaleString()} · {(l.lots || []).length} lot{(l.lots || []).length > 1 ? 's' : ''}</span>
          : <span className="stock-chip none">—</span>}
      </div>
      <div><span className={'status-pill ' + l.status}>{l.status}</span></div>
    </div>
  );
}

/* ---- Orders board ---- */
function OrderRow({ o, onSelectJob }) {
  const [open, setOpen] = useState(false);
  const total = o.lines.reduce((s, l) => s + l.qty, 0);
  const allocated = o.lines.reduce((s, l) => s + (l.allocatedQty || 0), 0);
  const soonest = o.lines.map((l) => l.requestedDate).filter(Boolean).sort()[0];
  return (
    <div className="order">
      <div className="order-head" onClick={() => setOpen(!open)}>
        <div>
          <div><span className="jno">{o.orderNo}</span>{o.customerPo ? <span style={{ fontSize: 11, color: 'var(--fg-subtle)' }}> · PO {o.customerPo}</span> : null}</div>
          <div className="jmeta">{o.customer}{o.currency ? ` · ${o.currency}` : ''} · {o.lines.length} line{o.lines.length > 1 ? 's' : ''}</div>
        </div>
        <div className="qty">{total.toLocaleString()} u</div>
        <div>
          {allocated > 0
            ? <span className="stock-chip">{allocated.toLocaleString()} allocated from FM store</span>
            : <span className="stock-chip none">nothing allocated</span>}
        </div>
        <DueCell iso={soonest} />
      </div>
      {open && (
        <div className="order-lines">
          {o.lines.map((l) => <OrderLineRow key={l.lineNo} l={l} onSelectJob={onSelectJob} />)}
        </div>
      )}
    </div>
  );
}

function OrdersBoard({ orders, onSelectJob }) {
  return (
    <div className="board">
      <div className="row head" style={{ gridTemplateColumns: 'minmax(220px, 1.3fr) 110px minmax(200px, 1.2fr) 120px' }}>
        <div>Sales order</div><div>Qty</div><div>FM store</div><div>Requested</div>
      </div>
      {orders.map((o) => <OrderRow key={o.orderNo} o={o} onSelectJob={onSelectJob} />)}
      {orders.length === 0 && <div className="empty">No open sales orders.</div>}
    </div>
  );
}

/* ---- Sales orders page ----
 * Read-only dashboard over every open sales order, in the concept's
 * OrdersDashboard shape (stat tiles · issues feed · orders table).
 * The only system check for now: an open line for an item whose
 * item_masters.stock_on_hold is P or F slipped through the capture
 * gate. Credit and QC checks are out of scope at this stage. */
function AlertIcon({ size = 13 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M10.3 3.3 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.3a2 2 0 0 0-3.4 0z" />
      <path d="M12 9v4" /><path d="M12 17h.01" />
    </svg>
  );
}

const CCY_SYMBOL = { USD: '$', EUR: '€', GBP: '£', R: 'R', ZAR: 'R' };
function fmtValue(v, ccy) {
  return (CCY_SYMBOL[ccy] || (ccy ? ccy + ' ' : '')) + Math.round(v).toLocaleString('en-US');
}

function analyzeSalesOrder(o) {
  const held = o.lines.filter(lineHeld);
  let value = 0, priced = false;
  for (const l of o.lines) if (l.unitPrice != null) { value += l.qty * l.unitPrice; priced = true; }
  const requested = o.lines.map((l) => l.requestedDate).filter(Boolean).sort()[0] || null;
  const qty = o.lines.reduce((s, l) => s + l.qty, 0);
  return { ...o, held, value: priced ? value : null, requested, qty };
}

function SalesOrderRow({ o, open, onToggle, onSelectJob }) {
  return (
    <div className={'order' + (o.held.length ? ' has-hold' : '')} id={'so-' + o.orderNo}>
      <div className="order-head so-head" onClick={onToggle}>
        <div>
          <div><span className="jno">{o.orderNo.replace(/^0+/, '')}</span>{o.customerPo ? <span style={{ fontSize: 11, color: 'var(--fg-subtle)' }}> · PO {o.customerPo}</span> : null}</div>
          <div className="jmeta">{o.customer}{o.incoterms ? ` · ${o.incoterms}` : ''} · {o.lines.length} line{o.lines.length > 1 ? 's' : ''}</div>
        </div>
        <div className="qty">{o.qty.toLocaleString()} u</div>
        <div>
          {o.value != null
            ? <><div className="ord-val">{fmtValue(o.value, o.currency)}</div><div className="jmeta">{o.currency || '—'}</div></>
            : <span className="stock-chip none">unpriced</span>}
        </div>
        <DueCell iso={o.requested} />
        <div>
          {o.held.length
            ? <span className="badge badge-danger"><span className="dot" />{o.held.length} held line{o.held.length > 1 ? 's' : ''}</span>
            : <span className="badge badge-ok"><span className="dot" />Clear</span>}
        </div>
      </div>
      {open && (
        <div className="order-lines">
          {o.lines.map((l) => <OrderLineRow key={l.lineNo} l={l} onSelectJob={onSelectJob} />)}
        </div>
      )}
    </div>
  );
}

/* Stat tiles shared by the plan board and sales orders page. */
function StatTiles({ stats }) {
  return (
    <div className="stats">
      {stats.map((s) => (
        <div className="stat" key={s.lbl}>
          <div className="accent" style={{ background: s.accent }} />
          <div className="lbl">{s.lbl}</div>
          <div className="num">{s.num}</div>
          {s.sub && <div className={'sub' + (s.dn ? ' dn' : '')}>{s.sub}</div>}
        </div>
      ))}
    </div>
  );
}

function SalesOrdersPage({ orders, onSelectJob }) {
  const [filter, setFilter] = useState('all'); // all | holds
  const [query, setQuery] = useState('');
  const [openOrders, setOpenOrders] = useState({});

  const analyzed = useMemo(() => orders.map(analyzeSalesOrder), [orders]);
  const issues = analyzed.flatMap((o) => o.held.map((l) => ({ order: o, line: l })));
  const heldOrders = analyzed.filter((o) => o.held.length);

  const overdue = (o) => o.requested && daysFromToday(o.requested) < 0;
  const stats = [
    { lbl: 'Open orders', num: analyzed.length, sub: `${analyzed.reduce((s, o) => s + o.lines.length, 0)} lines`, accent: 'var(--brand)' },
    { lbl: 'Overdue', num: analyzed.filter(overdue).length, sub: 'past requested date', accent: 'var(--warning)' },
    { lbl: 'Orders with held items', num: heldOrders.length, sub: `${issues.length} held line${issues.length === 1 ? '' : 's'}`, accent: 'var(--danger)', dn: issues.length > 0 },
    { lbl: 'Clear orders', num: analyzed.length - heldOrders.length, sub: 'no stock-hold issues', accent: 'var(--gem-emerald)' },
  ];

  const q = query.trim().toLowerCase();
  const shown = analyzed.filter((o) => {
    if (filter === 'holds' && !o.held.length) return false;
    if (q && !(`${o.orderNo} ${o.customer} ${o.customerPo} ` +
      o.lines.map((l) => `${l.fCode} ${l.description}`).join(' ')).toLowerCase().includes(q)) return false;
    return true;
  });
  const sorted = [...shown].sort((a, b) =>
    (b.held.length ? 1 : 0) - (a.held.length ? 1 : 0) ||
    (a.requested || '9999').localeCompare(b.requested || '9999') ||
    a.orderNo.localeCompare(b.orderNo));

  function revealOrder(orderNo) {
    setOpenOrders((cur) => ({ ...cur, [orderNo]: true }));
    // the row exists after this render tick — then bring it into view
    requestAnimationFrame(() => {
      const el = document.getElementById('so-' + orderNo);
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    });
  }

  return (
    <>
      <StatTiles stats={stats} />

      <div className="ord-grid">
        <div className="panel ord-issues">
          <div className="panel-head">
            <div>
              <h3>Order issues</h3>
              <div className="psub">Open lines for items the item master has on stock hold.</div>
            </div>
            {issues.length > 0 && <span className="tally"><span className="d" />{issues.length}</span>}
          </div>
          {issues.length === 0 && (
            <div className="ord-clear"><Check />All open orders clear — no held items detected.</div>
          )}
          {issues.map((i, idx) => (
            <div className="ord-issue" key={idx} onClick={() => revealOrder(i.order.orderNo)}>
              <span className="ord-issue-ic"><AlertIcon /></span>
              <div style={{ minWidth: 0 }}>
                <div className="ord-issue-head">
                  <span className="ord-issue-type">Stock hold · {i.line.stockOnHold}</span>
                  <span className="ord-issue-oid">{i.order.orderNo.replace(/^0+/, '')} · {i.order.customer}</span>
                </div>
                <div className="ord-issue-msg">
                  <b>{i.line.fCode}</b>{i.line.description ? ` ${i.line.description}` : ''} is on stock hold ({i.line.stockOnHold}),
                  yet line {i.line.lineNo} is open — {Number(i.line.qty).toLocaleString()} u
                  {i.line.requestedDate ? ` requested ${fmtDate(i.line.requestedDate)}` : ''} · {i.line.status}
                </div>
              </div>
            </div>
          ))}
        </div>

        <div className="panel ord-table">
          <div className="panel-head">
            <div>
              <h3>Open sales orders</h3>
              <div className="psub">Every open order line, straight from the v1 sync — click a row for its lines.</div>
            </div>
            <div className="seg">
              <button className={filter === 'all' ? 'on' : ''} onClick={() => setFilter('all')}>All</button>
              <button className={filter === 'holds' ? 'on' : ''} onClick={() => setFilter('holds')}>With holds</button>
            </div>
          </div>
          <div className="search" style={{ marginBottom: 10 }}>
            <input placeholder="Search order, customer, PO or stock code…" value={query} onChange={(e) => setQuery(e.target.value)} />
          </div>
          <div className="board">
            <div className="row head so-head">
              <div>Sales order</div><div>Qty</div><div>Value</div><div>Requested</div><div>Status</div>
            </div>
            {sorted.map((o) => (
              <SalesOrderRow key={o.orderNo} o={o} open={!!openOrders[o.orderNo]}
                onToggle={() => setOpenOrders((cur) => ({ ...cur, [o.orderNo]: !cur[o.orderNo] }))}
                onSelectJob={onSelectJob} />
            ))}
            {sorted.length === 0 && <div className="empty">No orders match.</div>}
          </div>
        </div>
      </div>
    </>
  );
}

/* ---- App shell: sidebar nav + topbar (concept's Sidebar/Topbar) ---- */
const NAV_ICONS = {
  timeline: <><rect x="3" y="4" width="11" height="3" rx="1" /><rect x="7" y="10.5" width="13" height="3" rx="1" /><rect x="3" y="17" width="9" height="3" rx="1" /></>,
  sales: <><path d="M4 3h2l2.4 12.4a1 1 0 0 0 1 .6h8.2a1 1 0 0 0 1-.8L21 8H6" /><circle cx="9" cy="20" r="1" /><circle cx="18" cy="20" r="1" /></>,
  chevdown: <path d="m6 9 6 6 6-6" />,
  lock: <><rect x="4" y="10" width="16" height="11" rx="2" /><path d="M8 10V7a4 4 0 0 1 8 0v3" /></>,
  user: <><circle cx="12" cy="8" r="4" /><path d="M4 21c0-4 3.6-6 8-6s8 2 8 6" /></>,
};

function Icon({ name, size = 17, style }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
      strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" style={style}>
      {NAV_ICONS[name]}
    </svg>
  );
}

const NAV = [
  { group: 'Planning', short: 'Plan' },
  { id: 'plan', label: 'Plan board', icon: 'timeline' },
  { group: 'Operations', short: 'Ops' },
  { id: 'sales', label: 'Sales orders', icon: 'sales' },
];

function Sidebar({ active, onNav, counts, collapsed, setCollapsed }) {
  return (
    <nav className="pp-side">
      <div className="brand">
        <img className="full" src="assets/logos/NUtec-short-cmyk.png" alt="NUtec" />
        <img className="mark" src="assets/logos/NU-mark.png" alt="nu" title="Expand" onClick={() => setCollapsed(false)} />
        <button className="toggle" title="Collapse" aria-label="Collapse navigation" onClick={() => setCollapsed(!collapsed)}>
          <Icon name="chevdown" size={13} style={{ transform: collapsed ? 'rotate(-90deg)' : 'rotate(90deg)' }} />
        </button>
      </div>
      {NAV.map((row, i) =>
        row.group ? (
          <div className="sec" key={'g' + i}>
            <span className="sec-full">{row.group}</span>
            <span className="sec-short">{row.short}</span>
          </div>
        ) : (
          <button key={row.id}
            className={'pp-link' + (active === row.id ? ' active' : '')}
            onClick={() => onNav(row.id)}
            title={collapsed ? row.label : undefined}>
            <span className="ic"><Icon name={row.icon} /></span>
            <span className="lbl">{row.label}</span>
            {counts[row.id] != null && <span className="count">{counts[row.id]}</span>}
          </button>
        )
      )}
      <div style={{ flex: 1 }} />
      <div className="side-foot">Go slice · read-only</div>
    </nav>
  );
}

/* The browser's IANA timezone gives the location without a geolocation
 * permission prompt (e.g. Africa/Johannesburg → Johannesburg). */
const LOCAL_CITY = (Intl.DateTimeFormat().resolvedOptions().timeZone || '')
  .split('/').pop().replace(/_/g, ' ') || 'Local';

function useClock() {
  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const t = setInterval(() => setNow(new Date()), 30000);
    return () => clearInterval(t);
  }, []);
  return now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
}

function Topbar({ crumbs }) {
  const time = useClock();
  return (
    <div className="pp-top">
      <div className="crumbs">
        {crumbs.map((c, i) => (
          <React.Fragment key={i}>
            {i > 0 && <span className="sep">/</span>}
            {i === crumbs.length - 1 ? <strong>{c}</strong> : <span>{c}</span>}
          </React.Fragment>
        ))}
      </div>
      <div className="right">
        <span style={{ fontFamily: 'var(--font-mono)' }}>{LOCAL_CITY} · {time}</span>
        <span className="role-pill" title="This slice is read-only — no planner mutations yet">
          <Icon name="lock" size={12} />Viewer
        </span>
        <span className="avatar" title="Sign-in comes with the write slice"><Icon name="user" size={16} /></span>
      </div>
    </div>
  );
}

/* ---- App root ---- */
const PAGE_TITLES = {
  plan: ['Planning', 'Plan board'],
  sales: ['Operations', 'Sales orders'],
};

function pageFromHash() { return window.location.hash === '#sales' ? 'sales' : 'plan'; }

function App() {
  const [page, setPage] = useState(pageFromHash);
  const [collapsed, setCollapsed] = useState(false);
  const [view, setView] = useState('job');
  const [jobs, setJobs] = useState(null);
  const [orders, setOrders] = useState(null);
  const [err, setErr] = useState(null);
  const [query, setQuery] = useState('');
  const [families, setFamilies] = useState([]);
  const [hideCaptured, setHideCaptured] = useState(false);
  const [showSamples, setShowSamples] = useState(false);
  const [selectedJob, setSelectedJob] = useState(null);

  const [updatedAt, setUpdatedAt] = useState(null);
  const [refreshing, setRefreshing] = useState(false);

  const load = React.useCallback(() => {
    setRefreshing(true);
    Promise.all([
      fetch('/api/plan/jobs').then((r) => { if (!r.ok) throw new Error(`jobs: HTTP ${r.status}`); return r.json(); }),
      fetch('/api/plan/orders').then((r) => { if (!r.ok) throw new Error(`orders: HTTP ${r.status}`); return r.json(); }),
    ]).then(([j, o]) => { setJobs(j.jobs || []); setOrders(o.orders || []); setUpdatedAt(new Date()); setErr(null); })
      .catch((e) => setErr(String(e)))
      .finally(() => setRefreshing(false));
  }, []);

  // Load on mount, then keep the board live: poll every 60 s (the underlying
  // tables are refreshed by v1's sync workers on a similar cadence).
  useEffect(() => {
    load();
    const t = setInterval(load, 60000);
    return () => clearInterval(t);
  }, [load]);

  // Pages are hash-routed (#plan / #sales) so the back button and deep links work.
  useEffect(() => {
    function onHash() { setPage(pageFromHash()); }
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);
  function nav(p) { window.location.hash = p; setPage(p); }

  const famOpts = useMemo(() => jobs ? [...new Set(jobs.map((j) => j.family).filter(Boolean))].sort() : [], [jobs]);

  const planJobs = useMemo(() => jobs ? jobs.filter((j) => showSamples || !j.sample) : [], [jobs, showSamples]);
  const groups = useMemo(() => groupJobs(planJobs), [planJobs]);

  // Filters act on whole groups: a group stays if any member matches, so a
  // blend job always appears alongside its products.
  const shownGroups = useMemo(() => {
    const q = query.trim().toLowerCase();
    return groups.filter((g) => {
      if (q && !g.members.some((j) => `${j.job} ${j.stockCode} ${j.description}`.toLowerCase().includes(q))) return false;
      if (families.length && !g.members.some((j) => families.includes(j.family))) return false;
      if (hideCaptured && g.lead.phase === 'Captured') return false;
      return true;
    });
  }, [groups, query, families, hideCaptured]);

  if (err) return <div className="shell"><div className="error">Failed to load: {err}</div></div>;
  if (!jobs || !orders) return <div className="shell"><div className="loading">Loading live plan data…</div></div>;

  const kpis = [
    { lbl: 'Open jobs', num: planJobs.length, accent: 'var(--brand)' },
    { lbl: 'On the floor', num: planJobs.filter((j) => j.phaseIdx > 0 && j.phase !== 'FM Store').length, accent: 'var(--gem-sapphire)' },
    { lbl: 'In FM store', num: planJobs.filter((j) => j.phase === 'FM Store').length, accent: 'var(--gem-emerald)' },
    { lbl: 'Open orders', num: orders.length, accent: 'var(--gem-quartz)' },
  ];
  const sampleCount = jobs.filter((j) => j.sample).length;
  const navCounts = {
    plan: planJobs.length,
    sales: orders.filter((o) => o.lines.some(lineHeld)).length || null,
  };

  return (
    <div className={'pp-shell' + (collapsed ? ' collapsed' : '')}>
      <Sidebar active={page} onNav={nav} counts={navCounts} collapsed={collapsed} setCollapsed={setCollapsed} />
      <div className="pp-main">
      <Topbar crumbs={['NUtec', ...PAGE_TITLES[page]]} />
      <div className="pp-body">
      <div className="shell">
      <div className="masthead">
        <h1>{PAGE_TITLES[page][1]}</h1>
        <span className="tag">Go slice · live data</span>
        <span className="refresh-box">
          {updatedAt && <span className="refresh-at">updated {updatedAt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}</span>}
          <button className="chip" onClick={load} disabled={refreshing} title="Refreshes automatically every 60 seconds">
            {refreshing ? 'Refreshing…' : 'Refresh'}
          </button>
        </span>
      </div>
      <p className="sub">
        {page === 'sales'
          ? 'Every open sales order, checked against item-master stock holds — read-only.'
          : 'Reading nutec_factory (maintained by the v1 sync) — read-only vertical slice.'}
      </p>

      {page === 'sales' && <SalesOrdersPage orders={orders} onSelectJob={setSelectedJob} />}

      {page === 'plan' && <>
      <StatTiles stats={kpis} />

      <div className="toolbar">
        <div className="seg">
          <button className={view === 'job' ? 'on' : ''} onClick={() => setView('job')}>By job</button>
          <button className={view === 'order' ? 'on' : ''} onClick={() => setView('order')}>By sales order</button>
        </div>
        {view === 'job' && (
          <>
            <div className="search">
              <input placeholder="Search job, stock code or description…" value={query} onChange={(e) => setQuery(e.target.value)} />
            </div>
            {famOpts.map((f) => (
              <button key={f} className={'chip' + (families.includes(f) ? ' on' : '')}
                onClick={() => setFamilies((cur) => cur.includes(f) ? cur.filter((x) => x !== f) : [...cur, f])}>
                <span className="sw" style={{ background: famColor(f) }} />{f}
              </button>
            ))}
            <button className={'chip' + (hideCaptured ? ' on' : '')} onClick={() => setHideCaptured(!hideCaptured)}>Hide captured</button>
            {sampleCount > 0 && (
              <button className={'chip' + (showSamples ? ' on' : '')} onClick={() => setShowSamples(!showSamples)}
                title="Retained-sample jobs (S-suffix) are off the plan by default">
                Samples · {sampleCount}
              </button>
            )}
            <span className="count">{shownGroups.length}/{groups.length} groups</span>
          </>
        )}
      </div>

      {view === 'job' && (
        <div className="legend">
          {PHASES.map((p) => <span className="lg" key={p}><span className="sw" style={{ background: PHASE_COLOR[p] }} />{p}</span>)}
        </div>
      )}

      {view === 'job' ? <JobsBoard groups={shownGroups} onSelect={setSelectedJob} /> : <OrdersBoard orders={orders} onSelectJob={setSelectedJob} />}
      </>}

      <p className="footnote">
        {page === 'sales'
          ? <>Hold flags come straight from item_masters.stock_on_hold (SysPro InvMaster hold status, P or F).
              Credit and QC checks are out of scope for this slice.</>
          : <>Phases derive from real events only: WipMaster capture, riteTIME work-centre scans (via the governed
              work-centre→phase map) and FM-store receipt movements. No fabricated percentages.</>}
      </p>
      </div>
      </div>
      </div>

      <Drawer jobNo={selectedJob} onClose={() => setSelectedJob(null)} onSelect={setSelectedJob} />
    </div>
  );
}

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