/* global React, MegaFooter */
//
// Consignments portal — two top-level pages.
//   ConsignmentsAdminPage   /rentals/consignments         (admin UI, magic-link auth, @valley.film only)
//   ConsignorPortalPage     /consignments/<token>         (consignor read-only view)
//
// Single file because both views share several small UI bits (currency
// formatters, ledger table, the request/preview/commit flow). Mounted
// from app.jsx and components-rentals.jsx via window globals at the
// bottom of this file.

const { useState: useStateCon, useEffect: useEffectCon, useRef: useRefCon } = React;

// ---------- formatters ----------

const fmtGbp = (n) => {
  const v = Number(n);
  if (!Number.isFinite(v)) return '£0.00';
  const sign = v < 0 ? '-' : '';
  return `${sign}£${Math.abs(v).toFixed(2)}`;
};

const fmtPct = (n) => {
  const v = Number(n);
  if (!Number.isFinite(v)) return '0%';
  return `${Math.round(v * 100)}%`;
};

const fmtDate = (iso) => {
  if (!iso) return '';
  try {
    const d = new Date(iso);
    return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
  } catch { return iso; }
};

const todayIso = () => new Date().toISOString().slice(0, 10);

// ---------- API client ----------

async function adminApi(action, payload = {}, { method = 'POST', query = '' } = {}) {
  const url = `/api/consignments/admin${query ? '?' + query : ''}`;
  const init = {
    method,
    headers: {},
    // Explicit so private-browsing / tracker-protection modes don't strip
    // the vf_admin session cookie. Same-origin only, never cross-origin.
    credentials: 'same-origin',
  };
  if (method !== 'GET') {
    init.headers['Content-Type'] = 'application/json';
    init.body = JSON.stringify({ action, ...payload });
  }
  const r = await fetch(url, init);
  const body = await r.json().catch(() => ({}));
  return { status: r.status, ok: r.ok && body.ok !== false, body };
}

// ===================================================================
// /rentals/consignments  —  admin entry
// ===================================================================
function ConsignmentsAdminPage({ onGoto }) {
  const [stage, setStage] = useStateCon('loading'); // loading | login | sent | dashboard
  const [email, setEmail] = useStateCon('');
  const [loginError, setLoginError] = useStateCon('');
  const [adminEmail, setAdminEmail] = useStateCon('');

  // On mount: check session via GET ?action=session. Cookie travels
  // automatically since the call is same-origin.
  useEffectCon(() => {
    fetch('/api/consignments/admin?action=session', { method: 'GET', credentials: 'same-origin' })
      .then((r) => r.json().then((b) => ({ status: r.status, body: b })))
      .then(({ status, body }) => {
        if (status === 200 && body.ok) {
          // Consignors get bounced to their portal — the admin
          // dashboard is staff-only. Admin role lands here directly.
          if (body.role === 'consignor') {
            window.location.href = '/consignments';
            return;
          }
          setAdminEmail(body.email);
          setStage('dashboard');
        } else {
          setStage('login');
        }
      })
      .catch(() => setStage('login'));

    // If the URL has ?login_error=... (set by the verify endpoint when
    // a stale magic link is clicked), surface that message instead of
    // the generic login form.
    const params = new URLSearchParams(window.location.search);
    const err = params.get('login_error');
    if (err) {
      setLoginError('Your sign-in link has expired or is invalid. Request a new one below.');
      // Strip the param so a refresh doesn't keep re-showing the error.
      const cleanUrl = window.location.pathname;
      try { window.history.replaceState(null, '', cleanUrl); } catch (e) { /* */ }
    }
  }, []);

  const sendLink = async (e) => {
    e.preventDefault();
    setLoginError('');
    if (!email.trim()) { setLoginError('Enter your email.'); return; }
    const { ok } = await adminApi('login_request', { email: email.trim() });
    if (ok) setStage('sent');
    else setLoginError('Could not send the sign-in email. Please try again.');
  };

  const logout = async () => {
    await adminApi('logout');
    setAdminEmail('');
    setStage('login');
  };

  return (
    <main className="page active rentals-light rentals-consignments-admin" data-screen-label="04x Consignments · Admin">
      <section className="rentals-subhero">
        <div className="container">
          <div className="eyebrow"><span className="idx">VR—10</span> Consignments admin</div>
          <h1>Consignment <em>accounts</em>.</h1>
          {stage === 'dashboard' && (
            <div className="cn-admin-status">
              <span>Signed in as <strong>{adminEmail}</strong></span>
              <button type="button" className="cn-link-btn" onClick={logout}>Sign out</button>
            </div>
          )}
        </div>
      </section>

      {stage === 'loading' && (
        <section className="cn-section">
          <div className="container"><p className="cn-loading">Loading…</p></div>
        </section>
      )}

      {stage === 'login' && (
        <section className="cn-section">
          <div className="container cn-login-container">
            <p className="lead">Sign in to your consignment account — use the email registered with us and we'll send a one-time sign-in link.</p>
            {loginError && <div className="cn-error">{loginError}</div>}
            <form onSubmit={sendLink} className="cn-login-form">
              <label>
                <span className="cn-label">Your email</span>
                <input
                  type="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  placeholder="you@example.com"
                  autoFocus
                  required
                />
              </label>
              <button type="submit" className="cn-btn-primary">Send sign-in link</button>
            </form>
          </div>
        </section>
      )}

      {stage === 'sent' && (
        <section className="cn-section">
          <div className="container cn-login-container">
            <p className="lead">Check your inbox.</p>
            <p>We've sent a sign-in link to <strong>{email}</strong>. It's valid for 15 minutes.</p>
            <p className="cn-muted">If the email doesn't arrive, check your spam folder — or come back here and request another one.</p>
            <button type="button" className="cn-link-btn" onClick={() => setStage('login')}>← Back</button>
          </div>
        </section>
      )}

      {stage === 'dashboard' && (
        <AdminDashboard adminEmail={adminEmail} />
      )}

      <MegaFooter isRentals onGoto={onGoto} />
    </main>
  );
}

// ---------- dashboard ----------

function AdminDashboard({ adminEmail }) {
  const [consignors, setConsignors] = useStateCon(null);
  const [selectedId, setSelectedId] = useStateCon(null);
  const [loadError, setLoadError] = useStateCon('');
  const [refreshTick, setRefreshTick] = useStateCon(0);
  const [issues, setIssues] = useStateCon(null);

  useEffectCon(() => {
    setLoadError('');
    fetch('/api/consignments/admin?action=list_consignors', { method: 'GET' })
      .then((r) => r.json().then((b) => ({ status: r.status, body: b })))
      .then(({ status, body }) => {
        if (status === 200 && body.ok) {
          setConsignors(body.consignors || []);
          // Auto-select first consignor on first load.
          if (!selectedId && body.consignors?.length) setSelectedId(body.consignors[0].id);
        } else {
          setLoadError(body?.error || `Could not load consignors (status ${status}).`);
        }
      })
      .catch((err) => setLoadError(err?.message || 'Network error.'));
    fetch('/api/consignments/admin?action=list_issues', { method: 'GET' })
      .then((r) => r.json().then((b) => ({ status: r.status, body: b })))
      .then(({ status, body }) => {
        if (status === 200 && body.ok) setIssues(body);
      })
      .catch(() => { /* non-fatal */ });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [refreshTick]);

  const refreshAll = () => setRefreshTick((t) => t + 1);

  const selected = consignors?.find((c) => c.id === selectedId) || null;

  if (loadError) {
    return (
      <section className="cn-section">
        <div className="container">
          <div className="cn-error">Loading failed: {loadError}</div>
        </div>
      </section>
    );
  }

  if (!consignors) {
    return (
      <section className="cn-section">
        <div className="container"><p className="cn-loading">Loading consignors…</p></div>
      </section>
    );
  }

  return (
    <section className="cn-section">
      <div className="container">
        {issues && (issues.issues?.length > 0 || issues.untaggedCount > 0) && (
          <IssuesBanner issues={issues} />
        )}
        <div className="cn-dashboard">
          <aside className="cn-sidebar">
            <div className="cn-sidebar-head">
              <h3>Consignors</h3>
              <button
                type="button"
                className="cn-link-btn"
                onClick={async () => {
                  const name = window.prompt('Consignor name?');
                  if (!name) return;
                  const { ok, body } = await adminApi('create_consignor', { data: { name, active: true } });
                  if (ok) { setSelectedId(body.consignor.id); refreshAll(); }
                  else window.alert('Failed: ' + (body?.error || 'unknown'));
                }}
              >
                + Add
              </button>
            </div>
            <ul className="cn-consignor-list">
              {consignors.map((c) => (
                <li
                  key={c.id}
                  className={'cn-consignor-row' + (c.id === selectedId ? ' is-selected' : '')}
                  onClick={() => setSelectedId(c.id)}
                >
                  <div className="cn-consignor-name">{c.name}</div>
                  <div className={'cn-consignor-bal ' + (c.balance >= 0 ? 'is-pos' : 'is-neg')}>
                    {fmtGbp(c.balance)}
                  </div>
                </li>
              ))}
              {consignors.length === 0 && (
                <li className="cn-muted" style={{ padding: '10px 0' }}>No consignors yet.</li>
              )}
            </ul>
          </aside>

          <div className="cn-main">
            {selected
              ? <ConsignorDetail consignor={selected} adminEmail={adminEmail} onAnyChange={refreshAll} />
              : <p className="cn-muted">Select a consignor on the left.</p>}
          </div>
        </div>
      </div>
    </section>
  );
}

// ---------- issues banner ----------

function IssuesBanner({ issues }) {
  const [open, setOpen] = useStateCon(false);
  const total = (issues.issues || []).length;
  return (
    <div className="cn-issues-banner">
      <div className="cn-issues-summary">
        <strong>
          Booqable items map ·
          {' '}{issues.validCount} tagged
          {' · '}{issues.untaggedCount} untagged
          {total > 0 && <span className="cn-issues-bad"> · {total} need attention</span>}
        </strong>
        <div className="cn-issues-meta">
          <span className="cn-muted">Last sync: {issues.syncedAt || 'never'}</span>
          {total > 0 && (
            <button type="button" className="cn-link-btn" onClick={() => setOpen((o) => !o)}>
              {open ? 'Hide' : 'Show'} items
            </button>
          )}
        </div>
      </div>
      {open && total > 0 && (
        <table className="cn-issues-table">
          <thead><tr><th>Item</th><th>Marker</th><th>Issue</th></tr></thead>
          <tbody>
            {issues.issues.map((i, idx) => (
              <tr key={idx}>
                <td>{i.name}<div className="cn-muted" style={{ fontSize: 12 }}>{i.slug}</div></td>
                <td><code>{i.ownerType}: {i.ownerName}</code></td>
                <td>{i.kind === 'unknown_name' ? 'No matching consignor row' : 'Unrecognised marker type'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

// ---------- consignor detail ----------

function ConsignorDetail({ consignor, adminEmail, onAnyChange }) {
  const [detail, setDetail] = useStateCon(null);
  const [loadError, setLoadError] = useStateCon('');
  const [editing, setEditing] = useStateCon(false);
  const [editValues, setEditValues] = useStateCon(null);
  const [showUpload, setShowUpload] = useStateCon(false);
  const [showAddEntry, setShowAddEntry] = useStateCon(false);
  const [sendingLink, setSendingLink] = useStateCon(false);
  const [linkUrl, setLinkUrl] = useStateCon('');
  const [booqableItems, setBooqableItems] = useStateCon(null);

  const load = async () => {
    setLoadError('');
    const { status, body } = await adminApi('consignor', {}, { method: 'GET', query: `action=consignor&id=${encodeURIComponent(consignor.id)}` });
    if (status === 200 && body.ok) {
      setDetail(body);
    } else {
      setLoadError(body?.error || `Status ${status}`);
    }
    const r = await adminApi('items_for_consignor', {}, { method: 'GET', query: `action=items_for_consignor&id=${encodeURIComponent(consignor.id)}` });
    if (r.status === 200 && r.body.ok) setBooqableItems(r.body);
  };

  useEffectCon(() => { load(); /* eslint-disable-next-line */ }, [consignor.id]);

  const startEdit = () => {
    setEditValues({
      name: detail.consignor.name,
      email: detail.consignor.email || '',
      phone: detail.consignor.phone || '',
      commissionVfPct: detail.consignor.commissionVfPct ?? 0,
      donationPct: detail.consignor.donationPct ?? 0,
      donationRecipient: detail.consignor.donationRecipient || '',
      notionPage: detail.consignor.notionPage || '',
      active: detail.consignor.active !== false,
      notes: detail.consignor.notes || '',
    });
    setEditing(true);
  };

  const saveEdit = async () => {
    const { ok, body } = await adminApi('update_consignor', {
      id: consignor.id,
      updates: {
        ...editValues,
        // Number inputs come back as strings — coerce.
        commissionVfPct: Number(editValues.commissionVfPct) || 0,
        donationPct: Number(editValues.donationPct) || 0,
      },
    });
    if (ok) {
      setEditing(false);
      await load();
      onAnyChange();
    } else {
      window.alert('Save failed: ' + (body?.error || 'unknown'));
    }
  };

  const sendLink = async () => {
    setSendingLink(true);
    const { ok, body } = await adminApi('send_link', { consignorId: consignor.id });
    setSendingLink(false);
    if (ok) {
      setLinkUrl(body.url);
      window.alert(`Link sent to ${detail.consignor.email}\n\n${body.url}`);
    } else {
      window.alert('Send failed: ' + (body?.error || 'unknown'));
    }
  };

  const regenLink = async () => {
    if (!window.confirm('Generate a new link? The previous link will stop working.')) return;
    const { ok, body } = await adminApi('regenerate_token', { consignorId: consignor.id });
    if (ok) {
      setLinkUrl(body.url);
      window.prompt('New link (copy this):', body.url);
    } else {
      window.alert('Regen failed: ' + (body?.error || 'unknown'));
    }
  };

  if (loadError) return <div className="cn-error">Load failed: {loadError}</div>;
  if (!detail) return <p className="cn-loading">Loading…</p>;

  const c = detail.consignor;
  const consignorSharePct = 1 - (c.commissionVfPct || 0) - (c.donationPct || 0);

  return (
    <div className="cn-detail">
      {/* Profile */}
      <div className="cn-detail-head">
        <div>
          <h2 className="cn-detail-name">{c.name}</h2>
          <div className="cn-detail-sub">
            {c.email ? <a href={`mailto:${c.email}`}>{c.email}</a> : <span className="cn-muted">No email</span>}
            {c.phone && <span style={{ marginLeft: 14 }}>{c.phone}</span>}
          </div>
        </div>
        <div className="cn-detail-balance">
          <div className="cn-bal-label">Balance owed</div>
          <div className={'cn-bal-value ' + (c.balance >= 0 ? 'is-pos' : 'is-neg')}>{fmtGbp(c.balance)}</div>
        </div>
      </div>

      <div className="cn-card">
        <div className="cn-card-head">
          <h3>Profile & commission</h3>
          {!editing && <button type="button" className="cn-link-btn" onClick={startEdit}>Edit</button>}
        </div>
        {editing ? (
          <ProfileEditor
            values={editValues}
            onChange={(v) => setEditValues({ ...editValues, ...v })}
            onSave={saveEdit}
            onCancel={() => setEditing(false)}
          />
        ) : (
          <dl className="cn-kv">
            <dt>Commission to VF</dt><dd>{fmtPct(c.commissionVfPct)}</dd>
            <dt>Donation</dt>       <dd>{c.donationPct > 0 ? `${fmtPct(c.donationPct)} to ${c.donationRecipient || '—'}` : <span className="cn-muted">—</span>}</dd>
            <dt>Consignor share</dt><dd><strong>{fmtPct(consignorSharePct)}</strong></dd>
            <dt>Active</dt>         <dd>{c.active ? 'Yes' : 'No'}</dd>
            <dt>Notion page</dt>    <dd>{c.notionPage ? <a href={c.notionPage} target="_blank" rel="noopener noreferrer">Open ↗</a> : <span className="cn-muted">—</span>}</dd>
            {c.notes && <><dt>Notes</dt><dd className="cn-notes">{c.notes}</dd></>}
          </dl>
        )}
      </div>

      <div className="cn-card">
        <div className="cn-card-head">
          <h3>Consignor link</h3>
          <div className="cn-card-actions">
            <button type="button" className="cn-btn-secondary" onClick={sendLink} disabled={!c.email || sendingLink}>
              {sendingLink ? 'Sending…' : 'Email link to consignor'}
            </button>
            <button type="button" className="cn-link-btn" onClick={regenLink}>Generate new link</button>
          </div>
        </div>
        {!c.email && <p className="cn-muted">Add an email to the profile to enable sending.</p>}
        {linkUrl && (
          <div className="cn-link-display">
            <span className="cn-muted">Latest link:</span>{' '}
            <a href={linkUrl} target="_blank" rel="noopener noreferrer">{linkUrl}</a>
          </div>
        )}
      </div>

      <div className="cn-card">
        <div className="cn-card-head">
          <h3>Booqable items {booqableItems ? `(${booqableItems.items.length})` : ''}</h3>
          {booqableItems?.syncedAt && (
            <div className="cn-muted" style={{ fontSize: 12 }}>Synced {booqableItems.syncedAt}</div>
          )}
        </div>
        {!booqableItems ? (
          <p className="cn-muted">Loading…</p>
        ) : booqableItems.items.length === 0 ? (
          <p className="cn-muted">No Booqable items tagged with this consignor yet. Add a line at the bottom of each item's description in Booqable: <code>{detail.consignor.name === 'Valley Films' ? 'Own' : 'Consignor'}: {detail.consignor.name}</code></p>
        ) : (
          <ul className="cn-booqable-items">
            {booqableItems.items.map((it) => (
              <li key={it.slug}>
                <span className="cn-pill cn-pill-small">{it.ownerType}</span>
                <span className="cn-booqable-item-name">{it.name}</span>
                <span className="cn-muted" style={{ fontSize: 12 }}>{it.category} · £{(it.dayRate ?? 0).toFixed(2)}/day</span>
              </li>
            ))}
          </ul>
        )}
      </div>

      <div className="cn-card">
        <div className="cn-card-head">
          <h3>Activity ({detail.ledger.length})</h3>
          <div className="cn-card-actions">
            <button type="button" className="cn-btn-secondary" onClick={() => setShowAddEntry(true)}>+ Manual entry</button>
            <button type="button" className="cn-btn-primary" onClick={() => setShowUpload(true)}>↑ Upload Booqable CSV</button>
          </div>
        </div>
        <LedgerTable
          ledger={detail.ledger}
          onUpdated={() => { load(); onAnyChange(); }}
        />
      </div>

      {showUpload && (
        <CSVUploadModal
          consignor={c}
          onClose={() => setShowUpload(false)}
          onCommitted={() => { setShowUpload(false); load(); onAnyChange(); }}
        />
      )}
      {showAddEntry && (
        <ManualEntryModal
          consignorId={consignor.id}
          adminEmail={adminEmail}
          onClose={() => setShowAddEntry(false)}
          onSaved={() => { setShowAddEntry(false); load(); onAnyChange(); }}
        />
      )}
    </div>
  );
}

// ---------- profile editor ----------

function ProfileEditor({ values, onChange, onSave, onCancel }) {
  return (
    <div className="cn-form">
      <div className="cn-form-row">
        <label><span className="cn-label">Name</span>
          <input value={values.name} onChange={(e) => onChange({ name: e.target.value })} />
        </label>
      </div>
      <div className="cn-form-row cn-form-2col">
        <label><span className="cn-label">Email</span>
          <input type="email" value={values.email} onChange={(e) => onChange({ email: e.target.value })} />
        </label>
        <label><span className="cn-label">Phone</span>
          <input value={values.phone} onChange={(e) => onChange({ phone: e.target.value })} />
        </label>
      </div>
      <div className="cn-form-row cn-form-3col">
        <label><span className="cn-label">Commission to VF (decimal: 0.50 = 50%)</span>
          <input type="number" step="0.01" min="0" max="1" value={values.commissionVfPct}
            onChange={(e) => onChange({ commissionVfPct: e.target.value })} />
        </label>
        <label><span className="cn-label">Donation %</span>
          <input type="number" step="0.01" min="0" max="1" value={values.donationPct}
            onChange={(e) => onChange({ donationPct: e.target.value })} />
        </label>
        <label><span className="cn-label">Donation recipient</span>
          <input value={values.donationRecipient} onChange={(e) => onChange({ donationRecipient: e.target.value })} />
        </label>
      </div>
      <div className="cn-form-row">
        <label><span className="cn-label">Notion page URL</span>
          <input value={values.notionPage} onChange={(e) => onChange({ notionPage: e.target.value })} />
        </label>
      </div>
      <div className="cn-form-row">
        <label className="cn-form-inline">
          <input type="checkbox" checked={values.active}
            onChange={(e) => onChange({ active: e.target.checked })} />
          <span>Active</span>
        </label>
      </div>
      <div className="cn-form-row">
        <label><span className="cn-label">Notes</span>
          <textarea rows={3} value={values.notes} onChange={(e) => onChange({ notes: e.target.value })} />
        </label>
      </div>
      <div className="cn-form-actions">
        <button type="button" className="cn-btn-primary" onClick={onSave}>Save</button>
        <button type="button" className="cn-link-btn" onClick={onCancel}>Cancel</button>
      </div>
    </div>
  );
}

// ---------- ledger table ----------

function LedgerTable({ ledger, onUpdated }) {
  const [editingId, setEditingId] = useStateCon(null);
  const [editValues, setEditValues] = useStateCon(null);

  if (!ledger || ledger.length === 0) {
    return <p className="cn-muted">No activity yet.</p>;
  }

  const startEdit = (e) => {
    setEditingId(e.id);
    setEditValues({
      entry: e.entry,
      date: e.date || todayIso(),
      amount: e.amount,
      type: e.type || 'Adjustment',
      period: e.period || '',
      notes: e.notes || '',
    });
  };

  const saveEdit = async () => {
    const { ok, body } = await adminApi('update_ledger', {
      id: editingId,
      updates: { ...editValues, amount: Number(editValues.amount) },
    });
    if (ok) { setEditingId(null); onUpdated(); }
    else window.alert('Save failed: ' + (body?.error || 'unknown'));
  };

  const removeEntry = async (entryId) => {
    if (!window.confirm('Archive this ledger entry? It will be hidden from the consignor and recoverable from Notion trash for 30 days.')) return;
    const { ok, body } = await adminApi('delete_ledger', { id: entryId });
    if (ok) onUpdated();
    else window.alert('Delete failed: ' + (body?.error || 'unknown'));
  };

  return (
    <div className="cn-table-wrap">
      <table className="cn-ledger-table">
        <thead>
          <tr>
            <th style={{ width: 110 }}>Date</th>
            <th>Entry</th>
            <th style={{ width: 120 }}>Type</th>
            <th style={{ width: 100, textAlign: 'right' }}>Amount</th>
            <th style={{ width: 100 }}>Source</th>
            <th style={{ width: 80 }}></th>
          </tr>
        </thead>
        <tbody>
          {ledger.map((e) => {
            const isEditing = editingId === e.id;
            return (
              <React.Fragment key={e.id}>
                <tr className={isEditing ? 'is-editing' : ''}>
                  {isEditing ? (
                    <>
                      <td><input type="date" value={editValues.date} onChange={(ev) => setEditValues({ ...editValues, date: ev.target.value })} /></td>
                      <td><input value={editValues.entry} onChange={(ev) => setEditValues({ ...editValues, entry: ev.target.value })} /></td>
                      <td>
                        <select value={editValues.type} onChange={(ev) => setEditValues({ ...editValues, type: ev.target.value })}>
                          <option>Opening Balance</option>
                          <option>Earnings</option>
                          <option>Payout</option>
                          <option>Purchase</option>
                          <option>Admin Fee</option>
                          <option>Adjustment</option>
                        </select>
                      </td>
                      <td><input type="number" step="0.01" value={editValues.amount} onChange={(ev) => setEditValues({ ...editValues, amount: ev.target.value })} style={{ textAlign: 'right' }} /></td>
                      <td>{e.source}</td>
                      <td>
                        <button type="button" className="cn-link-btn" onClick={saveEdit}>Save</button>
                        {' · '}
                        <button type="button" className="cn-link-btn" onClick={() => setEditingId(null)}>×</button>
                      </td>
                    </>
                  ) : (
                    <>
                      <td>{fmtDate(e.date)}</td>
                      <td>
                        <div className="cn-ledger-entry">{e.entry}</div>
                        {e.period && <div className="cn-muted" style={{ fontSize: 12 }}>{e.period}</div>}
                      </td>
                      <td><span className={'cn-pill cn-pill-' + (e.type || '').toLowerCase().replace(/\s+/g, '-')}>{e.type}</span></td>
                      <td className={'cn-amount ' + (e.amount >= 0 ? 'is-pos' : 'is-neg')}>{fmtGbp(e.amount)}</td>
                      <td className="cn-muted">{e.source}</td>
                      <td>
                        <button type="button" className="cn-link-btn" onClick={() => startEdit(e)}>Edit</button>
                        {' · '}
                        <button type="button" className="cn-link-btn" onClick={() => removeEntry(e.id)}>Del</button>
                      </td>
                    </>
                  )}
                </tr>
                {(e.notes || (e.csvFiles && e.csvFiles.length > 0)) && !isEditing && (
                  <tr className="cn-ledger-meta-row">
                    <td colSpan={6}>
                      {e.notes && <div className="cn-ledger-notes">{e.notes}</div>}
                      {e.csvFiles?.length > 0 && (
                        <div className="cn-ledger-files">
                          {e.csvFiles.map((f, i) => (
                            <span key={i} className="cn-file-chip">
                              {f.url ? <a href={f.url} target="_blank" rel="noopener noreferrer">{f.name}</a> : f.name}
                            </span>
                          ))}
                        </div>
                      )}
                    </td>
                  </tr>
                )}
              </React.Fragment>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

// ---------- CSV upload modal ----------

function CSVUploadModal({ consignor, onClose, onCommitted }) {
  const [file, setFile] = useStateCon(null);
  const [csvText, setCsvText] = useStateCon('');
  const [preview, setPreview] = useStateCon(null);
  const [previewError, setPreviewError] = useStateCon('');
  const [period, setPeriod] = useStateCon('');
  const [label, setLabel] = useStateCon('');
  const [date, setDate] = useStateCon(todayIso());
  const [committing, setCommitting] = useStateCon(false);
  const fileInputRef = useRefCon(null);

  const onFile = async (f) => {
    setFile(f);
    setPreview(null);
    setPreviewError('');
    if (!f) return;
    const text = await f.text();
    setCsvText(text);
    const { ok, body } = await adminApi('preview_csv', { consignorId: consignor.id, csv: text });
    if (ok) {
      setPreview(body);
      // Auto-suggest period from filename like "March Report - JC.csv" or "report-rentals-export-2026-04-30.csv"
      const monthMatch = (f.name || '').match(/\b(jan(uary)?|feb(ruary)?|mar(ch)?|apr(il)?|may|jun(e)?|jul(y)?|aug(ust)?|sep(tember)?|oct(ober)?|nov(ember)?|dec(ember)?)\b/i);
      const yearMatch = (f.name || '').match(/\b(20\d{2})\b/);
      const suggestedPeriod = monthMatch
        ? `${monthMatch[0].charAt(0).toUpperCase()}${monthMatch[0].slice(1).toLowerCase()} ${yearMatch?.[1] || new Date().getFullYear()}`
        : `${new Date().toLocaleString('en-GB', { month: 'long' })} ${new Date().getFullYear()}`;
      setPeriod(suggestedPeriod);
      setLabel(`${suggestedPeriod} earnings`);
    } else {
      setPreviewError(body?.detail || body?.error || 'Could not parse CSV.');
    }
  };

  const commit = async () => {
    setCommitting(true);
    const { ok, body } = await adminApi('commit_entry', {
      consignorId: consignor.id,
      csv: csvText,
      csvFilename: file?.name,
      period,
      label,
      date,
    });
    setCommitting(false);
    if (ok) {
      onCommitted();
    } else {
      window.alert('Commit failed: ' + (body?.detail || body?.error || 'unknown'));
    }
  };

  return (
    <div className="cn-modal-backdrop" onClick={onClose}>
      <div className="cn-modal" onClick={(e) => e.stopPropagation()}>
        <div className="cn-modal-head">
          <h3>Upload Booqable CSV — {consignor.name}</h3>
          <button type="button" className="cn-link-btn" onClick={onClose}>Close</button>
        </div>

        <div className="cn-modal-body">
          <p className="cn-muted">
            Export the rentals "Product performance" report from Booqable for the period you want to settle.
            Drop the CSV in — we'll sum the revenue and apply this consignor's split ({fmtPct(consignor.commissionVfPct)} to VF
            {consignor.donationPct > 0 && `, ${fmtPct(consignor.donationPct)} to ${consignor.donationRecipient}`},
            {' '}<strong>{fmtPct(1 - consignor.commissionVfPct - (consignor.donationPct || 0))}</strong> to consignor).
          </p>

          <div className="cn-upload-row">
            <input
              type="file"
              accept=".csv,text/csv"
              ref={fileInputRef}
              onChange={(e) => onFile(e.target.files?.[0])}
            />
          </div>

          {previewError && <div className="cn-error">{previewError}</div>}

          {preview && (
            <div className="cn-preview">
              <div className="cn-preview-totals">
                <div className="cn-preview-stat">
                  <span className="cn-muted">Products</span>
                  <strong>{preview.total.productCount}</strong>
                </div>
                <div className="cn-preview-stat">
                  <span className="cn-muted">Total rentals</span>
                  <strong>{preview.total.rentedCount}</strong>
                </div>
                <div className="cn-preview-stat">
                  <span className="cn-muted">Total revenue</span>
                  <strong>{fmtGbp(preview.total.revenuePounds)}</strong>
                </div>
                <div className="cn-preview-stat is-payout">
                  <span className="cn-muted">Payout to {consignor.name}</span>
                  <strong>{fmtGbp(preview.payout.consignorPounds)}</strong>
                </div>
              </div>

              <details className="cn-preview-rows">
                <summary>Show {preview.rows.length} CSV rows</summary>
                <table className="cn-csv-table">
                  <thead><tr><th>Product</th><th style={{ textAlign: 'right' }}>Rentals</th><th style={{ textAlign: 'right' }}>Revenue</th></tr></thead>
                  <tbody>
                    {preview.rows.map((r, i) => (
                      <tr key={i}>
                        <td>{r.name}</td>
                        <td style={{ textAlign: 'right' }}>{r.rentedCount}</td>
                        <td style={{ textAlign: 'right' }}>{fmtGbp(r.revenuePounds)}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </details>

              <div className="cn-form-row cn-form-3col" style={{ marginTop: 20 }}>
                <label><span className="cn-label">Period label</span>
                  <input value={period} onChange={(e) => setPeriod(e.target.value)} placeholder="e.g. March 2026" />
                </label>
                <label><span className="cn-label">Ledger entry label</span>
                  <input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="e.g. March 2026 earnings" />
                </label>
                <label><span className="cn-label">Entry date</span>
                  <input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
                </label>
              </div>

              <div className="cn-form-actions" style={{ marginTop: 16 }}>
                <button type="button" className="cn-btn-primary" onClick={commit} disabled={committing}>
                  {committing ? 'Writing…' : `Write ${fmtGbp(preview.payout.consignorPounds)} to ledger`}
                </button>
                <button type="button" className="cn-link-btn" onClick={onClose}>Cancel</button>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ---------- manual entry modal ----------

function ManualEntryModal({ consignorId, adminEmail, onClose, onSaved }) {
  const [data, setData] = useStateCon({
    entry: '',
    date: todayIso(),
    amount: '',
    type: 'Adjustment',
    period: '',
    notes: '',
  });
  const [saving, setSaving] = useStateCon(false);

  const save = async () => {
    if (!data.entry.trim()) { window.alert('Add a label.'); return; }
    if (data.amount === '' || isNaN(Number(data.amount))) { window.alert('Add an amount (positive credit, negative debit).'); return; }
    setSaving(true);
    const { ok, body } = await adminApi('create_ledger', {
      data: { ...data, amount: Number(data.amount), consignorId, source: 'Manual', createdBy: adminEmail },
    });
    setSaving(false);
    if (ok) onSaved();
    else window.alert('Save failed: ' + (body?.error || 'unknown'));
  };

  return (
    <div className="cn-modal-backdrop" onClick={onClose}>
      <div className="cn-modal cn-modal-small" onClick={(e) => e.stopPropagation()}>
        <div className="cn-modal-head">
          <h3>Add ledger entry</h3>
          <button type="button" className="cn-link-btn" onClick={onClose}>Close</button>
        </div>
        <div className="cn-modal-body">
          <div className="cn-form">
            <div className="cn-form-row">
              <label><span className="cn-label">Label</span>
                <input value={data.entry} onChange={(e) => setData({ ...data, entry: e.target.value })} placeholder="e.g. SD card purchase" />
              </label>
            </div>
            <div className="cn-form-row cn-form-3col">
              <label><span className="cn-label">Date</span>
                <input type="date" value={data.date} onChange={(e) => setData({ ...data, date: e.target.value })} />
              </label>
              <label><span className="cn-label">Type</span>
                <select value={data.type} onChange={(e) => setData({ ...data, type: e.target.value })}>
                  <option>Earnings</option>
                  <option>Payout</option>
                  <option>Purchase</option>
                  <option>Admin Fee</option>
                  <option>Adjustment</option>
                  <option>Opening Balance</option>
                </select>
              </label>
              <label><span className="cn-label">Amount (£)</span>
                <input type="number" step="0.01" value={data.amount} onChange={(e) => setData({ ...data, amount: e.target.value })} placeholder="−45.00 to debit" />
              </label>
            </div>
            <div className="cn-form-row">
              <label><span className="cn-label">Period (optional)</span>
                <input value={data.period} onChange={(e) => setData({ ...data, period: e.target.value })} placeholder="e.g. March 2026" />
              </label>
            </div>
            <div className="cn-form-row">
              <label><span className="cn-label">Notes</span>
                <textarea rows={3} value={data.notes} onChange={(e) => setData({ ...data, notes: e.target.value })} />
              </label>
            </div>
            <div className="cn-form-actions">
              <button type="button" className="cn-btn-primary" onClick={save} disabled={saving}>
                {saving ? 'Saving…' : 'Save'}
              </button>
              <button type="button" className="cn-link-btn" onClick={onClose}>Cancel</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ===================================================================
// /consignments/<token> (direct token URL) or /consignments (signed-in
// session) — read-only consignor view.
// ===================================================================
function ConsignorPortalPage({ onGoto, consignmentToken }) {
  const [data, setData] = useStateCon(null);
  const [loadError, setLoadError] = useStateCon('');
  const [signedInVia, setSignedInVia] = useStateCon(null); // 'token' | 'session'

  useEffectCon(() => {
    // Two auth paths to the same data:
    //   - With token in URL → /api/consignments/portal?token=... (stateless)
    //   - Without token     → /api/consignments/portal (reads session cookie)
    const url = consignmentToken
      ? `/api/consignments/portal?token=${encodeURIComponent(consignmentToken)}`
      : `/api/consignments/portal`;
    setSignedInVia(consignmentToken ? 'token' : 'session');
    fetch(url, { credentials: 'same-origin' })
      .then((r) => r.json().then((b) => ({ status: r.status, body: b })))
      .then(({ status, body }) => {
        if (status === 200 && body.ok) setData(body);
        else if (status === 401 && !consignmentToken) setLoadError('not_signed_in');
        else setLoadError(body?.error || `status_${status}`);
      })
      .catch(() => setLoadError('network'));
  }, [consignmentToken]);

  const signOut = async () => {
    await fetch('/api/consignments/admin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action: 'logout' }),
      credentials: 'same-origin',
    });
    window.location.href = '/rentals/consignments';
  };

  if (loadError) {
    const titles = {
      missing:                'This link is <em>incomplete</em>.',
      invalid_or_expired_token: 'This link has <em>expired</em>.',
      not_found:              'Account <em>not found</em>.',
      not_signed_in:          'Please <em>sign in</em>.',
      network:                'Trouble <em>loading</em> the page.',
    };
    const leads = {
      missing: 'The consignment URL is missing its token. If you arrived from an email, please use the original link.',
      invalid_or_expired_token: "We've moved on from this link. Email rentals@valley.film and we'll send you a fresh one.",
      not_found: "We couldn't find an active account for this link. If you believe this is in error, please email rentals@valley.film.",
      not_signed_in: 'Your sign-in has expired or you opened this page in a new browser. Click below to sign back in.',
      network: "We couldn't reach the server. Please refresh in a moment, or email rentals@valley.film if it keeps happening.",
    };
    return (
      <main className="page active rentals-light rentals-consignor-portal">
        <section className="rentals-subhero">
          <div className="container">
            <div className="eyebrow"><span className="idx">VR—11</span> Your consignment account</div>
            <h1 dangerouslySetInnerHTML={{ __html: titles[loadError] || 'This link is <em>invalid</em>.' }} />
            <p className="lead">{leads[loadError] || 'Please email rentals@valley.film if you believe this is in error.'}</p>
            {loadError === 'not_signed_in' && (
              <p style={{ marginTop: 18 }}>
                <a href="/rentals/consignments" className="cn-btn-primary" style={{ textDecoration: 'none', display: 'inline-block' }}>Sign in →</a>
              </p>
            )}
          </div>
        </section>
        <MegaFooter isRentals onGoto={onGoto} />
      </main>
    );
  }

  if (!data) {
    return (
      <main className="page active rentals-light rentals-consignor-portal">
        <section className="rentals-subhero">
          <div className="container">
            <div className="eyebrow"><span className="idx">VR—11</span> Your consignment account</div>
            <h1>Loading…</h1>
          </div>
        </section>
      </main>
    );
  }

  const c = data.consignor;
  const consignorShare = 1 - (c.commissionVfPct || 0) - (c.donationPct || 0);

  return (
    <main className="page active rentals-light rentals-consignor-portal" data-screen-label="04c Consignor portal">
      <section className="rentals-subhero">
        <div className="container">
          <div className="eyebrow"><span className="idx">VR—11</span> Your consignment account</div>
          <h1>Hi, <em>{c.name}</em>.</h1>
          <p className="lead">Here's a snapshot of your Valley Rentals consignment account — what you can invoice for, and a record of every entry.{signedInVia === 'token' ? " Keep this link safe; it's private to you." : ''}</p>
          {signedInVia === 'session' && (
            <div className="cn-admin-status">
              <span>Signed in as <strong>{c.email}</strong></span>
              <button type="button" className="cn-link-btn" onClick={signOut}>Sign out</button>
            </div>
          )}
        </div>
      </section>

      <section className="cn-section">
        <div className="container">
          <div className="cn-portal-balance-card">
            <div className="cn-portal-balance-label">Current balance</div>
            <div className={'cn-portal-balance-value ' + (data.balance >= 0 ? 'is-pos' : 'is-neg')}>
              {fmtGbp(data.balance)}
            </div>
            <div className="cn-portal-balance-sub">
              {data.balance > 0
                ? <>This is the amount Valley owes you. Send an invoice to <a href="mailto:accounts@valley.film">accounts@valley.film</a> when you'd like to be paid out.</>
                : data.balance < 0
                  ? <>This is the amount currently on your account against future earnings (e.g. recent hardware purchase). No action needed.</>
                  : <>Your account is settled.</>}
            </div>
          </div>

          <div className="cn-portal-split">
            <div className="cn-portal-split-stat">
              <span className="cn-muted">Your share</span>
              <strong>{fmtPct(consignorShare)}</strong>
            </div>
            <div className="cn-portal-split-stat">
              <span className="cn-muted">Valley's commission</span>
              <strong>{fmtPct(c.commissionVfPct)}</strong>
            </div>
            {c.donationPct > 0 && (
              <div className="cn-portal-split-stat">
                <span className="cn-muted">Donated to {c.donationRecipient || 'charity'}</span>
                <strong>{fmtPct(c.donationPct)}</strong>
              </div>
            )}
          </div>

          <h2 className="cn-portal-h2">Activity</h2>
          <PortalLedger ledger={data.ledger} />

          {data.items?.length > 0 && (
            <>
              <h2 className="cn-portal-h2">Your equipment</h2>
              <p className="cn-muted" style={{ margin: '-4px 0 18px', fontSize: '14px' }}>
                Everything we list on the Booqable shop with a <code style={{ background: 'rgba(9,94,223,0.08)', padding: '1px 6px', borderRadius: 4, fontSize: '12.5px', color: 'var(--vf-blue)' }}>Consignor: {data.consignor.name}</code> marker. {data.items.length} item{data.items.length === 1 ? '' : 's'} in your account.
              </p>
              {(() => {
                // Group by category so the list reads like the shop does.
                // Ordering: same alphabetical order, but if 'Cameras' or
                // 'Lenses' is present, hoist them to the top — they're the
                // visual hero categories.
                const groups = {};
                for (const it of data.items) {
                  const cat = (it.category || 'Other').trim() || 'Other';
                  (groups[cat] = groups[cat] || []).push(it);
                }
                const HOIST = ['Cameras', 'Lenses', 'Lighting', 'Audio'];
                const sorted = Object.keys(groups).sort((a, b) => {
                  const ai = HOIST.indexOf(a); const bi = HOIST.indexOf(b);
                  if (ai !== -1 && bi !== -1) return ai - bi;
                  if (ai !== -1) return -1;
                  if (bi !== -1) return 1;
                  return a.localeCompare(b);
                });
                return sorted.map((cat) => (
                  <section key={cat} className="cn-portal-equipment-group">
                    <h3 className="cn-portal-equipment-cat">{cat} <span className="cn-muted">· {groups[cat].length}</span></h3>
                    <div className="cn-portal-equipment-grid">
                      {groups[cat].map((it, i) => (
                        <article key={i} className="cn-portal-equipment-card">
                          <div className="cn-portal-equipment-thumb">
                            {it.image ? (
                              <img src={it.image} alt="" loading="lazy" />
                            ) : (
                              <div className="cn-portal-equipment-thumb-empty" aria-hidden="true">
                                <Icon name="camera" />
                              </div>
                            )}
                          </div>
                          <div className="cn-portal-equipment-body">
                            <div className="cn-portal-equipment-name">{it.name}</div>
                            <div className="cn-portal-equipment-meta">
                              {it.type && <span className="cn-pill cn-pill-small">{it.type}</span>}
                              {it.dayRate ? <span className="cn-portal-equipment-rate">{fmtGbp(it.dayRate)} / day</span> : null}
                            </div>
                          </div>
                        </article>
                      ))}
                    </div>
                  </section>
                ));
              })()}
            </>
          )}

          <div className="cn-portal-help">
            <p>Questions about your account? Reply to your link email or write to <a href="mailto:rentals@valley.film">rentals@valley.film</a>.</p>
          </div>
        </div>
      </section>

      <MegaFooter isRentals onGoto={onGoto} />
    </main>
  );
}

function PortalLedger({ ledger }) {
  if (!ledger || ledger.length === 0) {
    return <p className="cn-muted">No activity yet — once your first month's rentals come in, you'll see entries here.</p>;
  }
  // Reverse so newest first
  const sorted = [...ledger].sort((a, b) => (b.date || '').localeCompare(a.date || ''));
  return (
    <div className="cn-table-wrap">
      <table className="cn-ledger-table cn-portal-ledger">
        <thead>
          <tr>
            <th>Date</th>
            <th>Entry</th>
            <th style={{ textAlign: 'right' }}>Amount</th>
          </tr>
        </thead>
        <tbody>
          {sorted.map((e, i) => (
            <React.Fragment key={i}>
              <tr>
                <td>{fmtDate(e.date)}</td>
                <td>
                  <div className="cn-ledger-entry">{e.entry}</div>
                  {e.period && <div className="cn-muted" style={{ fontSize: 12 }}>{e.period}</div>}
                </td>
                <td className={'cn-amount ' + (e.amount >= 0 ? 'is-pos' : 'is-neg')}>{fmtGbp(e.amount)}</td>
              </tr>
              {e.notes && (
                <tr className="cn-ledger-meta-row">
                  <td colSpan={3}><div className="cn-ledger-notes">{e.notes}</div></td>
                </tr>
              )}
            </React.Fragment>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// Export to window — app.jsx and components-rentals.jsx pick these up
// after this script loads. Babel-standalone doesn't do ES modules at
// runtime, so the global-window pattern is how cross-file references
// work in this codebase (same as ReferencePage, KitPrepTechPage, etc.).
Object.assign(window, { ConsignmentsAdminPage, ConsignorPortalPage });
