/* ============================================================
   MOJ Installation Commissioning Checklist — Vega NZ
   Zero-build React 18 (UMD) + Babel standalone. No router, no npm.
   State: one active session object, mirrored to localStorage.
   ============================================================ */

const { useState, useEffect, useMemo, useRef, useCallback } = React;

const STORE_PREFIX = 'vega-moj-';
/* Saved projects live as ONE array under PROJECTS_KEY. The two legacy keys — a single
   project record, and the older multi-site array — are read once and migrated in, then
   left in place. listDrafts() skips all three, since none of them is a checklist draft. */
const PROJECTS_KEY = STORE_PREFIX + 'projects';
const LEGACY_PROJECT_KEY = STORE_PREFIX + 'project';
const LEGACY_SITES_KEY = STORE_PREFIX + 'sites';
const RESERVED_KEYS = [PROJECTS_KEY, LEGACY_PROJECT_KEY, LEGACY_SITES_KEY];

const TYPES = {
  video: {
    key: 'video',
    name: 'Video System Commissioning',
    blurb: 'Full video system commissioning sheet — cabling, displays, cameras, VC and control.',
    file: 'data/video.json',
    mode: 'triad',
  },
  sound: {
    key: 'sound',
    name: 'Courtroom Sound System Commissioning',
    blurb: 'Courtroom sound commissioning — acoustics, microphones, DSP, FTR and hearing assistance.',
    file: 'data/sound.json',
    mode: 'triad',
  },
  handover: {
    key: 'handover',
    name: 'Handover Checklist',
    blurb: 'Pre-handover walkthrough with the site contact, ending in a signed Acceptance Certificate.',
    file: 'data/handover.json',
    mode: 'steps',
  },
};

const STATUSES = ['YES', 'NO', 'N/A'];

/* ---------- helpers ---------- */

function uid() {
  const rand = Math.random().toString(36).slice(2, 8);
  return Date.now().toString(36) + '-' + rand;
}

function todayISO() {
  const d = new Date();
  const p = (n) => String(n).padStart(2, '0');
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
}

function nzDate(iso) {
  if (!iso) return '';
  const parts = String(iso).split('-');
  if (parts.length !== 3) return iso;
  return parts[2] + '/' + parts[1] + '/' + parts[0];
}

/* Group key for a section number: 3.2.1.4 -> 3.2.1 ; 3.3.1 -> 3.3 ; 3.1 -> 3.1 */
function groupKey(section) {
  const parts = String(section || '').split('.');
  return parts.length >= 3 ? parts.slice(0, -1).join('.') : parts.join('.');
}

function buildRows(type, data) {
  if (TYPES[type].mode === 'steps') {
    return data.steps.map((s) => ({
      id: 'step-' + s.step,
      no: String(s.step),
      text: s.text,
      group: 'Handover steps',
    }));
  }
  return data.items.map((it, i) => ({
    id: 'item-' + i + '-' + it.section,
    no: it.section,
    text: it.item,
    group: groupKey(it.section),
  }));
}

function groupRows(rows) {
  const out = [];
  const index = {};
  rows.forEach((r) => {
    if (!(r.group in index)) {
      index[r.group] = out.length;
      out.push({ key: r.group, rows: [] });
    }
    out[index[r.group]].rows.push(r);
  });
  return out;
}

/* ---------- persistence ---------- */

function storageKey(session) {
  return STORE_PREFIX + session.type + '-' + session.id;
}

function saveSession(session) {
  try {
    localStorage.setItem(storageKey(session), JSON.stringify(session));
  } catch (err) {
    console.warn('Could not save draft to localStorage', err);
  }
}

function listDrafts() {
  const drafts = [];
  for (let i = 0; i < localStorage.length; i++) {
    const k = localStorage.key(i);
    if (!k || k.indexOf(STORE_PREFIX) !== 0) continue;
    if (RESERVED_KEYS.indexOf(k) >= 0) continue;
    try {
      const s = JSON.parse(localStorage.getItem(k));
      if (s && s.id && TYPES[s.type]) drafts.push(s);
    } catch (err) {
      /* ignore malformed entry */
    }
  }
  drafts.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
  return drafts;
}

/* The header fields a session carries. A fresh session carries NO identifiers —
   every field is deliberately blank unless a saved site seeds them. */
function blankMeta() {
  return {
    site: '', courtroom: '', date: '', installer: '',
    client: '', location: '', ticketId: '',
  };
}

/* A fresh session carries NO identifiers of its own. The source JSON meta blocks are
   blank, so nothing is seeded from them — the project this checklist was started from
   is the only thing that fills the header in, and every field stays editable on site.
   `projectId` is what lets the project screen list the checklists in progress on it. */
function newSession(type, seed, projectId) {
  return {
    id: uid(),
    type: type,
    projectId: projectId || '',
    meta: Object.assign(blankMeta(), seed || {}),
    answers: {},
    certificate: { contact: '', signedDate: '', notes: '' },
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  };
}

/* ---------- projects ----------
   A project holds the job details a technician would otherwise retype on every
   checklist. Projects are stored on this device only, as one array under PROJECTS_KEY,
   entirely separate from the source checklists in data/*.json. Most jobs only ever have
   one project; the list simply means an old job can be reopened. */

function blankProject() {
  return normaliseProject({ id: uid() });
}

/* Every field defaults to '' so a project (or a legacy site record) saved by an earlier
   version — which only carried name/site/courtroom/leadTechnician/date/installer —
   still loads cleanly. */
function normaliseProject(raw) {
  const s = raw || {};
  return {
    id: s.id || uid(),
    name: s.name || '',
    client: s.client || '',
    site: s.site || '',
    courtroom: s.courtroom || '',
    location: s.location || '',
    address: s.address || '',
    contact: s.contact || '',
    leadTechnician: s.leadTechnician || '',
    installer: s.installer || '',
    date: s.date || '',
    ticketId: s.ticketId || '',
    crmLink: s.crmLink || '',
    notes: s.notes || '',
    updatedAt: s.updatedAt || '',
  };
}

function readArray(key) {
  try {
    const raw = JSON.parse(localStorage.getItem(key));
    return Array.isArray(raw) ? raw : [];
  } catch (err) {
    console.warn('Could not read ' + key, err);
    return [];
  }
}

/* Read the project list, folding in anything an earlier version left behind. The
   migration only runs while PROJECTS_KEY has never been written — testing for the key
   itself, not for a non-empty list, is what stops a project deleted on purpose from
   coming back off the legacy keys the next time the app loads. */
function loadProjects() {
  if (localStorage.getItem(PROJECTS_KEY) !== null) {
    return readArray(PROJECTS_KEY).map(normaliseProject);
  }

  const legacy = readArray(LEGACY_SITES_KEY);
  try {
    const single = JSON.parse(localStorage.getItem(LEGACY_PROJECT_KEY));
    if (single && typeof single === 'object') legacy.push(single);
  } catch (err) {
    /* ignore malformed legacy record */
  }
  if (!legacy.length) return [];
  return persistProjects(legacy.map(normaliseProject));
}

function persistProjects(projects) {
  try {
    localStorage.setItem(PROJECTS_KEY, JSON.stringify(projects));
  } catch (err) {
    console.warn('Could not save projects to localStorage', err);
  }
  return projects;
}

/* The runner has a single "Installer / lead technician" field, so the project's lead
   technician is the fallback when no separate installer is recorded. */
function metaFromSite(project) {
  return {
    site: project.site || '',
    courtroom: project.courtroom || '',
    date: project.date || '',
    installer: project.installer || project.leadTechnician || '',
    client: project.client || '',
    location: project.location || '',
    ticketId: project.ticketId || '',
  };
}

/* Title: the project name if there is one, otherwise the best identifier we hold. */
function projectTitle(project) {
  return project.name || project.site || project.courtroom || project.client ||
    'Untitled project';
}

function projectSubtitle(project) {
  return [project.site, project.courtroom].filter(Boolean).join(' · ');
}

/* ---------- small components ---------- */

function Field(props) {
  return (
    <label className="field">
      <span className="field__label">{props.label}</span>
      <input
        className="input"
        type={props.type || 'text'}
        value={props.value}
        placeholder={props.placeholder || ''}
        onChange={(e) => props.onChange(e.target.value)}
      />
    </label>
  );
}

function AreaField(props) {
  return (
    <label className="field field--wide">
      <span className="field__label">{props.label}</span>
      <textarea
        className="textarea textarea--sm"
        rows={props.rows || 2}
        value={props.value}
        placeholder={props.placeholder || ''}
        onChange={(e) => props.onChange(e.target.value)}
      />
      {props.hint ? <span className="field__hint">{props.hint}</span> : null}
    </label>
  );
}

function Segmented(props) {
  return (
    <div className="seg" role="group" aria-label={'Status for item ' + props.itemNo}>
      {STATUSES.map((s) => {
        const cls = s === 'YES' ? 'is-yes' : s === 'NO' ? 'is-no' : 'is-na';
        return (
          <button
            key={s}
            type="button"
            className={'seg__btn ' + cls}
            aria-pressed={props.value === s}
            onClick={() => props.onChange(props.value === s ? null : s)}
          >
            {s}
          </button>
        );
      })}
    </div>
  );
}

function ItemRow(props) {
  const { row, mode, answer, onChange } = props;
  const [open, setOpen] = useState(!!(answer && answer.comment));
  const status = answer ? answer.status : null;
  const done = answer ? !!answer.done : false;
  const answered = mode === 'steps' ? done : !!status;
  const failed = mode === 'triad' && status === 'NO';

  return (
    <div className={'item' + (answered ? ' is-answered' : '') + (failed ? ' is-fail' : '')}>
      <div className="item__no">{mode === 'steps' ? 'Step ' + row.no : row.no}</div>
      <p className="item__text">{row.text}</p>
      <div className="item__actions">
        {mode === 'triad' ? (
          <Segmented
            itemNo={row.no}
            value={status}
            onChange={(v) => onChange({ status: v })}
          />
        ) : (
          <button
            type="button"
            className="toggle"
            aria-pressed={done}
            onClick={() => onChange({ done: !done })}
          >
            <span className="toggle__box" aria-hidden="true">{done ? '✓' : ''}</span>
            {done ? 'Completed' : 'Mark complete'}
          </button>
        )}
        <button type="button" className="link-btn" onClick={() => setOpen(!open)}>
          {open ? 'Hide note' : mode === 'steps' ? 'Add note' : 'Add installer comment'}
        </button>
        {!open && answer && answer.comment ? (
          <span className="comment-flag">Note saved</span>
        ) : null}
      </div>
      {open ? (
        <div className="item__comment">
          <textarea
            className="textarea"
            placeholder={mode === 'steps' ? 'Note' : 'Installer comments'}
            value={(answer && answer.comment) || ''}
            onChange={(e) => onChange({ comment: e.target.value })}
          />
        </div>
      ) : null}
    </div>
  );
}

function Progress(props) {
  const pct = props.total ? Math.round((props.done / props.total) * 100) : 0;
  return (
    <div className="progress no-print">
      <div className="progress__head">
        <span className="t-h4">Progress</span>
        <span className="progress__count">{props.done} / {props.total} complete · {pct}%</span>
      </div>
      <div
        className="progress__track"
        role="progressbar"
        aria-valuenow={props.done}
        aria-valuemin="0"
        aria-valuemax={props.total}
      >
        <div className="progress__bar" style={{ width: pct + '%' }} />
      </div>
    </div>
  );
}

/* ---------- screens ---------- */

/* HOME — the entry point, and nothing more. No checklists live here: a checklist is
   always run against a project, so Home only adds a project or reopens a saved one. */
function Home(props) {
  const { projects } = props;

  return (
    <div>
      <div className="page-head">
        <p className="t-eyebrow">Ministry of Justice · Courtroom AV</p>
        <h1 className="t-h2">Commissioning checklists</h1>
        <p className="t-body" style={{ maxWidth: '62ch', marginTop: '12px' }}>
          Add the project once — court, courtroom, client, technician — then run the video,
          sound and handover checklists from it. Everything is saved to this device only and
          survives a page refresh. Nothing is sent anywhere.
        </p>
        <div className="row-wrap" style={{ marginTop: '20px' }}>
          <button type="button" className="btn" onClick={props.onAdd}>+ Add project</button>
        </div>
      </div>

      {projects.length === 0 ? (
        <div className="site-empty">
          <p className="t-h4">No projects yet</p>
          <p className="t-body-sm" style={{ maxWidth: '48ch', margin: '8px auto 16px' }}>
            Add a project to get started — the checklists are run from inside it, so the job
            details are only ever typed once.
          </p>
          <button type="button" className="btn btn--sm" onClick={props.onAdd}>+ Add project</button>
        </div>
      ) : (
        <div>
          <h2 className="t-h3 is-ink" style={{ marginBottom: '12px' }}>
            Your projects ({projects.length})
          </h2>
          <div className="site-grid">
            {projects.map((p) => (
              <div
                key={p.id}
                role="button"
                tabIndex={0}
                className="site-card"
                onClick={() => props.onOpen(p.id)}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); props.onOpen(p.id); }
                }}
              >
                <div className="site-card__top">
                  <span className="site-card__fill">
                    {p.date ? nzDate(p.date) : 'No date set'}
                  </span>
                  <button
                    type="button"
                    className="icon-btn"
                    aria-label={'Delete project ' + projectTitle(p)}
                    title="Delete project"
                    onClick={(e) => { e.stopPropagation(); props.onDelete(p); }}
                  >
                    🗑
                  </button>
                </div>
                <div className="site-card__title">{projectTitle(p)}</div>
                <div className="site-card__sub">
                  {projectSubtitle(p) || 'No court or courtroom set'}
                </div>
                <div className="site-card__foot">
                  {p.client ? <span className="chip">{p.client}</span> : null}
                  {p.ticketId ? <span className="chip">{p.ticketId}</span> : null}
                  <span className="site-card__go" aria-hidden="true">Open →</span>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

/* THE PROJECT SCREEN — the job details, and the three checklists that run against them.
   Fields save to the record on every keystroke; the checklist launchers sit below them. */
function ProjectScreen(props) {
  const { project, drafts, onPatch, onDelete } = props;
  const site = project;
  const set = (k) => (v) => onPatch({ [k]: v });

  return (
    <div>
      <div className="toolbar no-print">
        <button type="button" className="btn btn--ghost btn--sm" onClick={props.onBack}>
          Back to projects
        </button>
        <span className="saved-note">{props.savedNote}</span>
      </div>

      <div className="page-head">
        <p className="t-eyebrow">Project</p>
        <h1 className="t-h2">{projectTitle(project)}</h1>
        <p className="t-body-sm" style={{ marginTop: '8px' }}>
          Changes save to this device as you type.
        </p>
      </div>

      <div className="card" style={{ marginBottom: '20px' }}>
        <div className="t-h4" style={{ marginBottom: '12px' }}>Project &amp; job</div>
        <div className="meta-grid">
          <Field label="Project name" value={site.name} placeholder="e.g. Wellington DC — Court 3"
            onChange={set('name')} />
          <Field label="Client" value={site.client} placeholder="e.g. Ministry of Justice"
            onChange={set('client')} />
          <Field label="Court / site" value={site.site} placeholder="e.g. Tauranga District Court"
            onChange={set('site')} />
          <Field label="Courtroom" value={site.courtroom} placeholder="e.g. DC1.11"
            onChange={set('courtroom')} />
          <Field label="Location" value={site.location} placeholder="City / region"
            onChange={set('location')} />
          <AreaField label="Site address" value={site.address}
            placeholder="Street, suburb, city, postcode" onChange={set('address')} />
        </div>
      </div>

      <div className="card" style={{ marginBottom: '20px' }}>
        <div className="t-h4" style={{ marginBottom: '12px' }}>People &amp; references</div>
        <div className="meta-grid">
          <AreaField label="Primary client contact" value={site.contact}
            placeholder="Name, title, email, phone" hint="Client-side contact for handover"
            onChange={set('contact')} />
          <Field label="Lead technician / prepared by" value={site.leadTechnician}
            placeholder="Name" onChange={set('leadTechnician')} />
          <Field label="Installer" value={site.installer}
            placeholder="Name (defaults to lead technician)" onChange={set('installer')} />
          <Field label="Job / pre-sales ticket" value={site.ticketId} placeholder="e.g. PS-1042"
            onChange={set('ticketId')} />
          <Field label="CRM link" value={site.crmLink} placeholder="https://…"
            onChange={set('crmLink')} />
        </div>
      </div>

      <div className="card" style={{ marginBottom: '20px' }}>
        <div className="t-h4" style={{ marginBottom: '12px' }}>Schedule &amp; notes</div>
        <div className="meta-grid">
          <Field label="Default checklist date" type="date" value={site.date}
            onChange={set('date')} />
        </div>
        <div style={{ marginTop: '16px' }}>
          <label className="field">
            <span className="field__label">Notes</span>
            <textarea
              className="textarea"
              value={site.notes}
              placeholder="Access, keys, site rules, anything the next visit should know"
              onChange={(e) => onPatch({ notes: e.target.value })}
            />
          </label>
        </div>
      </div>

      <div className="no-print" style={{ marginBottom: '20px' }}>
        <h2 className="t-h3 is-ink" style={{ marginBottom: '4px' }}>Checklists</h2>
        <p className="t-body-sm" style={{ marginBottom: '16px', maxWidth: '62ch' }}>
          Start a checklist for this project. The header fields are seeded from the details
          above and stay editable on site.
        </p>
        <div className="pick-grid">
          {Object.keys(TYPES).map((k) => {
            const t = TYPES[k];
            const count = props.counts[k];
            return (
              <button
                key={k}
                type="button"
                className="card card--pick"
                onClick={() => props.onStart(k)}
              >
                <span className="pick-count">
                  {count == null ? 'Loading…' : count + (t.mode === 'steps' ? ' steps' : ' items')}
                </span>
                <span className="t-h3">{t.name}</span>
                <span className="t-body-sm">{t.blurb}</span>
                <span className="btn btn--sm" style={{ alignSelf: 'flex-start', marginTop: '8px' }}>
                  Start checklist
                </span>
              </button>
            );
          })}
        </div>
      </div>

      <div className="no-print" style={{ marginTop: '32px' }}>
        <h2 className="t-h3 is-ink" style={{ marginBottom: '12px' }}>Checklists in progress</h2>
        {drafts.length === 0 ? (
          <p className="t-body-sm">Nothing in progress for this project.</p>
        ) : (
          <div className="stack">
            {drafts.map((d) => (
              <div className="draft-row" key={d.id}>
                <div className="draft-row__meta">
                  <div className="t-h4">{TYPES[d.type].name}</div>
                  <div className="t-body-sm">
                    {(d.meta.site || 'No site') + ' · ' + (d.meta.courtroom || 'No courtroom') +
                      ' · ' + (nzDate(d.meta.date) || 'No date')}
                  </div>
                </div>
                <div className="row-wrap">
                  <button type="button" className="btn btn--sm" onClick={() => props.onResume(d)}>
                    Continue draft
                  </button>
                  <button
                    type="button"
                    className="btn btn--sm btn--ghost"
                    onClick={() => props.onDeleteDraft(d)}
                  >
                    Delete
                  </button>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>

      <div className="toolbar no-print" style={{ marginTop: '32px' }}>
        <button type="button" className="btn btn--ghost btn--sm" onClick={props.onBack}>
          Back to projects
        </button>
        <button type="button" className="btn btn--ghost btn--sm" onClick={() => onDelete(project)}>
          Delete project
        </button>
      </div>
    </div>
  );
}

function Runner(props) {
  const { session, rows, mode, onMeta, onAnswer } = props;
  const groups = useMemo(() => groupRows(rows), [rows]);
  const done = rows.filter((r) => {
    const a = session.answers[r.id];
    return a && (mode === 'steps' ? a.done : !!a.status);
  }).length;

  return (
    <div>
      <div className="toolbar no-print">
        <button type="button" className="btn btn--ghost btn--sm" onClick={props.onBack}>
          Back to project
        </button>
        <div className="row-wrap">
          <span className="saved-note">{props.savedNote}</span>
          <button type="button" className="btn btn--sm" onClick={props.onReport}>
            Review &amp; report
          </button>
        </div>
      </div>

      <div className="page-head">
        <p className="t-eyebrow">{mode === 'steps' ? 'Handover' : 'Commissioning'}</p>
        <h1 className="t-h2">{TYPES[session.type].name}</h1>
      </div>

      {/* Pre-filled from the project this checklist was started from, and editable here. */}
      <div className="card no-print" style={{ marginBottom: '20px' }}>
        <div className="meta-grid">
          <Field label="Site" value={session.meta.site} placeholder="Court site"
            onChange={(v) => onMeta('site', v)} />
          <Field label="Courtroom" value={session.meta.courtroom} placeholder="Room reference"
            onChange={(v) => onMeta('courtroom', v)} />
          <Field label="Date" type="date" value={session.meta.date}
            onChange={(v) => onMeta('date', v)} />
          <Field label="Installer / lead technician" value={session.meta.installer} placeholder="Name"
            onChange={(v) => onMeta('installer', v)} />
          <Field label="Client" value={session.meta.client} placeholder="e.g. Ministry of Justice"
            onChange={(v) => onMeta('client', v)} />
          <Field label="Location" value={session.meta.location} placeholder="City / region"
            onChange={(v) => onMeta('location', v)} />
          <Field label="Job / ticket ref" value={session.meta.ticketId} placeholder="e.g. PS-1042"
            onChange={(v) => onMeta('ticketId', v)} />
        </div>
      </div>

      <Progress done={done} total={rows.length} />

      {groups.map((g) => (
        <section className="group" key={g.key}>
          <div className="group__head">
            <span className="group__no">{g.key}</span>
            <span className="group__count">{g.rows.length} {g.rows.length === 1 ? 'item' : 'items'}</span>
          </div>
          {g.rows.map((r) => (
            <ItemRow
              key={r.id}
              row={r}
              mode={mode}
              answer={session.answers[r.id]}
              onChange={(patch) => onAnswer(r.id, patch)}
            />
          ))}
        </section>
      ))}

      <div className="toolbar no-print" style={{ marginTop: '24px' }}>
        <button type="button" className="btn btn--ghost btn--sm" onClick={props.onBack}>
          Back to project
        </button>
        <button type="button" className="btn" onClick={props.onReport}>
          Review &amp; report
        </button>
      </div>
    </div>
  );
}

/* ---------- report model ----------
   One description of the document, rendered twice: as the on-screen Report view
   and, by pdf.js, as the shared/downloaded A4 PDF. Keeping a single model is what
   stops the two surfaces drifting apart. */

const CERT_INTRO =
  'The completed checklist and signed Acceptance Certificate must be sent to the ' +
  'MoJ Sound System Project Manager within 2 working days of completion.';

const VMR_NOTE =
  'Identifiers are intentionally blank. Record the host and guest details for the ' +
  'test VMR on site as part of step 39.';

function statusOf(answer, mode) {
  if (mode === 'steps') {
    return answer && answer.done
      ? { label: 'Complete', kind: 'done' }
      : { label: 'Not done', kind: 'open' };
  }
  if (!answer || !answer.status) return { label: 'Not set', kind: 'open' };
  if (answer.status === 'YES') return { label: 'YES', kind: 'yes' };
  if (answer.status === 'NO') return { label: 'NO', kind: 'no' };
  return { label: 'N/A', kind: 'na' };
}

function vmrBlock(title, conn) {
  const c = conn || {};
  const d = c.connection_detail || {};
  return {
    title: title,
    rows: [
      { k: 'PIN', v: c.pin || '' },
      { k: 'VMR test', v: d.virtual_meeting_room_test || '' },
      { k: 'Web', v: d.web || '' },
      { k: 'Phone', v: d.phone || '' },
      { k: 'Methods', v: (c.connection_method || []).join(', ') },
    ],
  };
}

function buildReportModel(session, rows, mode, source) {
  const type = TYPES[session.type];
  const answered = rows.filter((r) => {
    const a = session.answers[r.id];
    return a && (mode === 'steps' ? a.done : !!a.status);
  }).length;

  const groups = groupRows(rows).map((g) => ({
    label: mode === 'steps' ? null : 'Section ' + g.key,
    rows: g.rows.map((r) => {
      const a = session.answers[r.id];
      const st = statusOf(a, mode);
      return {
        id: r.id,
        no: mode === 'steps' ? r.no : r.no,
        text: r.text,
        status: st.label,
        kind: st.kind,
        comment: (a && a.comment) || '',
      };
    }),
  }));

  const cert = (source && source.acceptance_certificate) || null;
  const vmr = cert && cert.vmr_details ? cert.vmr_details : null;

  /* The four core identifiers always appear (blank rules where empty, as in REV2).
     The site-record extras only appear when a site actually supplied them. */
  const meta = [
    { k: 'Site', v: session.meta.site },
    { k: 'Courtroom', v: session.meta.courtroom },
    { k: 'Date', v: nzDate(session.meta.date) },
    { k: 'Installer / lead technician', v: session.meta.installer },
  ];
  if (session.meta.client) meta.push({ k: 'Client', v: session.meta.client });
  if (session.meta.location) meta.push({ k: 'Location', v: session.meta.location });
  if (session.meta.ticketId) meta.push({ k: 'Job / ticket ref', v: session.meta.ticketId });
  meta.push({ k: 'Completion', v: answered + ' of ' + rows.length });
  meta.push({ k: 'Prepared by', v: 'Vega Global NZ' });

  return {
    logo: 'assets/vega_logo.png',
    eyebrow: 'Ministry of Justice · Courtroom AV',
    docTitle: type.name,
    docType: type.blurb,
    generated: nzDate(todayISO()),
    answered: answered,
    total: rows.length,
    meta: meta,
    columns: {
      ref: mode === 'steps' ? 'Step' : 'Ref',
      item: mode === 'steps' ? 'Handover step' : 'Checklist item',
      status: 'Status',
      comment: mode === 'steps' ? 'Note' : 'Installer comments',
    },
    groups: groups,
    certificate: mode === 'steps' && cert ? {
      title: 'Handover Acceptance Certificate',
      intro: CERT_INTRO,
      signatures: [
        { label: 'Site contact name', value: session.certificate.contact },
        { label: 'Date', value: nzDate(session.certificate.signedDate) },
        { label: 'Site contact signature', value: '' },
        { label: 'MoJ Sound System Project Manager sign-off', value: '' },
      ],
      vmr: vmr ? {
        note: VMR_NOTE,
        blocks: [
          vmrBlock('Host connection', vmr.host_connection),
          vmrBlock('Guest connection', vmr.guest_connection),
        ],
      } : null,
    } : null,
  };
}

function fileNameFor(model, session) {
  const bits = ['Vega', model.docTitle];
  if (session.meta.courtroom) bits.push(session.meta.courtroom);
  if (session.meta.date) bits.push(session.meta.date);
  return bits.join(' ').replace(/[^A-Za-z0-9]+/g, '-').replace(/^-|-$/g, '') + '.pdf';
}

/* ---------- report presentation ---------- */

function DocMetaCell(props) {
  return (
    <div className="dmeta__cell">
      <div className="dmeta__k">{props.k}</div>
      {props.v
        ? <div className="dmeta__v">{props.v}</div>
        : <div className="dmeta__blank" aria-label="to be completed on site" />}
    </div>
  );
}

function KvRow(props) {
  return (
    <div className="kv">
      <span className="kv__k">{props.k}</span>
      {props.v ? <span className="kv__v">{props.v}</span> : <span className="kv__blank" />}
    </div>
  );
}

function Report(props) {
  const { session, rows, mode, source } = props;
  const model = useMemo(
    () => buildReportModel(session, rows, mode, source),
    [session, rows, mode, source]
  );
  const [busy, setBusy] = useState(false);
  const [shareError, setShareError] = useState('');

  /* Web Share with files is mobile-only in practice; desktop falls back to download. */
  const canShareFiles = typeof navigator !== 'undefined' &&
    !!navigator.share && !!navigator.canShare && typeof File !== 'undefined';

  const shareLabel = canShareFiles ? 'Share' : 'Download PDF';

  const makePdf = useCallback(() => {
    if (!window.VegaPdf) return Promise.reject(new Error('PDF module not loaded'));
    return window.VegaPdf.build(model);
  }, [model]);

  const download = (blob, name) => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = name;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() => URL.revokeObjectURL(url), 4000);
  };

  const onShare = useCallback(() => {
    setShareError('');
    setBusy(true);
    makePdf().then((blob) => {
      const name = fileNameFor(model, session);
      const where = session.meta.courtroom || session.meta.site || 'MOJ Checklist';
      let file = null;
      try {
        file = new File([blob], name, { type: 'application/pdf' });
      } catch (err) { /* older browsers: no File constructor */ }

      if (file && navigator.canShare && navigator.canShare({ files: [file] })) {
        return navigator.share({
          title: model.docTitle + ' — ' + where,
          text: model.docTitle + ' — ' + where + '. Completed commissioning checklist attached.',
          files: [file],
        }).catch((err) => {
          if (err && err.name === 'AbortError') return;
          download(blob, name);
        });
      }
      download(blob, name);
    }).catch((err) => {
      console.error(err);
      setShareError('Could not build the PDF: ' + err.message + '. Use Print instead.');
    }).then(() => setBusy(false));
  }, [makePdf, model, session]);

  return (
    <div>
      <div className="toolbar no-print">
        <button type="button" className="btn btn--ghost btn--sm" onClick={props.onBack}>
          Back to checklist
        </button>
        <div className="row-wrap">
          <button type="button" className="btn btn--ghost btn--sm" onClick={props.onHome}>Home</button>
          <button type="button" className="btn btn--dark btn--sm" onClick={() => window.print()}>Print</button>
          <button type="button" className="btn" onClick={onShare} disabled={busy}>
            {busy ? 'Building PDF…' : shareLabel}
          </button>
        </div>
      </div>

      {shareError ? <p className="share-error no-print">{shareError}</p> : null}

      {mode === 'steps' && model.certificate ? (
        <div className="card no-print" style={{ marginBottom: '20px' }}>
          <div className="t-h4" style={{ marginBottom: '12px' }}>Acceptance certificate details</div>
          <div className="meta-grid">
            <Field label="Site contact name" value={session.certificate.contact}
              placeholder="Name" onChange={(v) => props.onCert('contact', v)} />
            <Field label="Date signed" type="date" value={session.certificate.signedDate}
              onChange={(v) => props.onCert('signedDate', v)} />
          </div>
        </div>
      ) : null}

      <div className="doc">
        <header className="doc__head">
          <img className="doc__logo" src="assets/vega_logo.png" alt="Vega" />
          <div className="doc__rule" />
          <p className="doc__eyebrow">{model.eyebrow}</p>
          <h1 className="doc__title">{model.docTitle}</h1>
          <p className="doc__type">{model.docType}</p>
        </header>

        <div className="dmeta">
          {model.meta.map((m) => <DocMetaCell key={m.k} k={m.k} v={m.v} />)}
        </div>

        <div className="doc__rule doc__rule--strong" />

        <div className="doc__tablewrap">
          <table className="dtable">
            <thead>
              <tr>
                <th className="dtable__ref">{model.columns.ref}</th>
                <th>{model.columns.item}</th>
                <th className="dtable__status">{model.columns.status}</th>
                <th className="dtable__comment">{model.columns.comment}</th>
              </tr>
            </thead>
            {model.groups.map((g, gi) => (
              <tbody key={g.label || 'g' + gi}>
                {g.label ? (
                  <tr className="dtable__section">
                    <th colSpan="4" scope="colgroup">{g.label}</th>
                  </tr>
                ) : null}
                {g.rows.map((r) => (
                  <tr key={r.id}>
                    <td className="dtable__ref">{r.no}</td>
                    <td className="dtable__item">{r.text}</td>
                    <td className="dtable__status">
                      <span className={'pill pill--' + r.kind}>{r.status}</span>
                    </td>
                    <td className="dtable__comment">{r.comment}</td>
                  </tr>
                ))}
              </tbody>
            ))}
          </table>
        </div>

        {model.certificate ? (
          <section className="dcert">
            <h2 className="dcert__title">{model.certificate.title}</h2>
            <p className="dcert__intro">{model.certificate.intro}</p>

            <div className="dsig">
              {model.certificate.signatures.map((s) => (
                <div className="dsig__cell" key={s.label}>
                  <div className="dsig__value">{s.value || ' '}</div>
                  <div className="dsig__line" />
                  <div className="dsig__label">{s.label}</div>
                </div>
              ))}
            </div>

            {model.certificate.vmr ? (
              <div className="dvmr">
                <h3 className="dvmr__title">Test VMR details</h3>
                <p className="dvmr__note">{model.certificate.vmr.note}</p>
                <div className="dvmr__grid">
                  {model.certificate.vmr.blocks.map((b) => (
                    <div className="dvmr__block" key={b.title}>
                      <div className="dvmr__head">{b.title}</div>
                      {b.rows.map((kv) => <KvRow key={kv.k} k={kv.k} v={kv.v} />)}
                    </div>
                  ))}
                </div>
              </div>
            ) : null}
          </section>
        ) : null}

        <footer className="doc__foot">
          <span>Vega Global NZ · Ministry of Justice courtroom AV</span>
          <span>Generated {model.generated}</span>
        </footer>
      </div>
    </div>
  );
}

/* ---------- app shell ---------- */

function App() {
  const [datasets, setDatasets] = useState({});
  const [error, setError] = useState(null);
  const [view, setView] = useState('home');
  const [session, setSession] = useState(null);
  const [drafts, setDrafts] = useState([]);
  const [sites, setSites] = useState([]);
  /* The project currently open: the record the project screen edits, and the one whose
     details seed any checklist started from it. */
  const [activeSiteId, setActiveSiteId] = useState('');
  const [savedNote, setSavedNote] = useState('');
  const [siteNote, setSiteNote] = useState('');
  const firstRender = useRef(true);

  useEffect(() => {
    Promise.all(
      Object.keys(TYPES).map((k) =>
        fetch(TYPES[k].file).then((r) => {
          if (!r.ok) throw new Error(TYPES[k].file + ' → HTTP ' + r.status);
          return r.json();
        }).then((d) => [k, d])
      )
    ).then((pairs) => {
      const out = {};
      pairs.forEach((p) => { out[p[0]] = p[1]; });
      setDatasets(out);
    }).catch((e) => setError(e.message));
    setDrafts(listDrafts());
    setSites(loadProjects());
  }, []);

  /* persist on every session change */
  useEffect(() => {
    if (!session) return;
    if (firstRender.current) { firstRender.current = false; }
    saveSession(session);
    setSavedNote('Draft saved');
    const t = setTimeout(() => setSavedNote(''), 1600);
    return () => clearTimeout(t);
  }, [session]);

  const counts = useMemo(() => {
    const c = {};
    Object.keys(TYPES).forEach((k) => {
      const d = datasets[k];
      c[k] = d ? (TYPES[k].mode === 'steps' ? d.steps.length : d.items.length) : null;
    });
    return c;
  }, [datasets]);

  /* What the open project screen lists: the drafts started from that project, plus any
     draft saved before checklists were linked to one — so nothing already on this device
     becomes unreachable. */
  const projectDrafts = useMemo(
    () => drafts.filter((d) => !d.projectId || d.projectId === activeSiteId),
    [drafts, activeSiteId]
  );

  const rows = useMemo(() => {
    if (!session || !datasets[session.type]) return [];
    return buildRows(session.type, datasets[session.type]);
  }, [session && session.type, datasets]);

  /* Every checklist starts from a project, so the header is seeded from that record —
     and stays editable in the runner. */
  const startFromSite = useCallback((type, site) => {
    setActiveSiteId(site.id);
    setSession(newSession(type, metaFromSite(site), site.id));
    setView('run');
    window.scrollTo(0, 0);
  }, []);

  /* Drafts saved before the header gained client/location/ticketId lack those keys. */
  const resume = useCallback((d) => {
    if (d.projectId) setActiveSiteId(d.projectId);
    setSession(Object.assign({}, d, { meta: Object.assign(blankMeta(), d.meta || {}) }));
    setView('run');
    window.scrollTo(0, 0);
  }, []);

  const removeDraft = useCallback((d) => {
    if (!window.confirm('Delete this draft? This cannot be undone.')) return;
    localStorage.removeItem(storageKey(d));
    setDrafts(listDrafts());
  }, []);

  const goHome = useCallback(() => {
    setSession(null);
    setDrafts(listDrafts());
    setView('home');
    window.scrollTo(0, 0);
  }, []);

  /* Leaving the runner returns to the project it was started from; the draft stays saved
     and is listed there under "Checklists in progress". */
  const backToProject = useCallback(() => {
    setSession(null);
    setDrafts(listDrafts());
    setView(activeSiteId ? 'project' : 'home');
    window.scrollTo(0, 0);
  }, [activeSiteId]);

  /* --- saved projects: create / update / remove, one localStorage key for the list --- */
  const flashSiteNote = useCallback((msg) => {
    setSiteNote(msg);
    setTimeout(() => setSiteNote(''), 1600);
  }, []);

  /* Live editing: patch the record in place, keeping list order stable while typing. */
  const patchSite = useCallback((id, patch) => {
    setSites((list) => persistProjects(list.map((s) => (
      s.id === id
        ? normaliseProject(Object.assign({}, s, patch, { updatedAt: new Date().toISOString() }))
        : s
    ))));
    flashSiteNote('Project saved');
  }, [flashSiteNote]);

  /* "Add project" goes straight to the project screen with a blank record — the details
     are entered once there, and the checklists are started from the same screen. */
  const createSite = useCallback(() => {
    const project = blankProject();
    setSites((list) => persistProjects(list.concat([project])));
    setActiveSiteId(project.id);
    setView('project');
    window.scrollTo(0, 0);
  }, []);

  const openSite = useCallback((id) => {
    setActiveSiteId(id);
    setView('project');
    window.scrollTo(0, 0);
  }, []);

  const deleteSite = useCallback((project) => {
    if (!window.confirm('Delete the project "' + projectTitle(project) +
      '"? This cannot be undone.')) return;
    setSites((list) => persistProjects(list.filter((s) => s.id !== project.id)));
    setActiveSiteId((cur) => (cur === project.id ? '' : cur));
    setSession((s) => (s && s.projectId === project.id ? null : s));
    setView((v) => (v === 'home' ? v : 'home'));
    window.scrollTo(0, 0);
  }, []);

  const setMeta = (k, v) =>
    setSession((s) => Object.assign({}, s, {
      meta: Object.assign({}, s.meta, { [k]: v }),
      updatedAt: new Date().toISOString(),
    }));

  const setAnswer = (id, patch) =>
    setSession((s) => Object.assign({}, s, {
      answers: Object.assign({}, s.answers, {
        [id]: Object.assign({}, s.answers[id], patch),
      }),
      updatedAt: new Date().toISOString(),
    }));

  const setCert = (k, v) =>
    setSession((s) => Object.assign({}, s, {
      certificate: Object.assign({}, s.certificate, { [k]: v }),
      updatedAt: new Date().toISOString(),
    }));

  let body;
  if (error) {
    body = (
      <div className="empty">
        <h1 className="t-h3">Checklist data could not be loaded</h1>
        <p className="t-body-sm">{error}</p>
        <p className="t-body-sm">Serve this folder over HTTP (for example <code>python -m http.server</code>) rather than opening the file directly.</p>
      </div>
    );
  } else if (view === 'home') {
    body = (
      <Home
        projects={sites}
        onAdd={createSite}
        onOpen={openSite}
        onDelete={deleteSite}
      />
    );
  } else if (view === 'project') {
    const active = sites.filter((s) => s.id === activeSiteId)[0];
    body = active ? (
      <ProjectScreen
        project={active}
        counts={counts}
        drafts={projectDrafts}
        savedNote={siteNote}
        onPatch={(patch) => patchSite(active.id, patch)}
        onDelete={deleteSite}
        onStart={(type) => startFromSite(type, active)}
        onResume={resume}
        onDeleteDraft={removeDraft}
        onBack={goHome}
      />
    ) : (
      <div className="empty">
        <h1 className="t-h3">That project is no longer saved</h1>
        <button type="button" className="btn btn--sm" onClick={goHome}>
          Back to projects
        </button>
      </div>
    );
  } else if (session) {
    const mode = TYPES[session.type].mode;
    body = view === 'report' ? (
      <Report
        session={session}
        rows={rows}
        mode={mode}
        source={datasets[session.type]}
        onCert={setCert}
        onBack={() => { setView('run'); window.scrollTo(0, 0); }}
        onHome={goHome}
      />
    ) : (
      <Runner
        session={session}
        rows={rows}
        mode={mode}
        savedNote={savedNote}
        onMeta={setMeta}
        onAnswer={setAnswer}
        onBack={backToProject}
        onReport={() => { setView('report'); window.scrollTo(0, 0); }}
      />
    );
  }

  return (
    <div className="app">
      <header className="app-header no-print">
        <div className="app-header__inner">
          <img className="app-header__logo" src="assets/vega_logo_white.png" alt="Vega" />
          <p className="app-header__title">MoJ Commissioning Checklist</p>
          <span className="app-header__spacer" />
        </div>
      </header>
      <main className="main">{body}</main>
    </div>
  );
}

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