// ════════════════════════════════════════════════════════════════════════
// PropMystro · M9 · pm-d-agreements.jsx  (faithful port of pm-agreement.jsx)
// Tenancy agreements: template library (your templates + law-maintained
// library), upload-your-own-AST → Claude reads the REAL clauses → placeholder
// mapping (auto-guessed sources) → saved template; clause preview; and the
// agreement builder — template → clause review → live-filled draft from the
// property/household records → e-sign → filed to Storage + documents +
// tenancies.agreement_doc_id. Tables: templates · documents · tenancies.
// → window.PMAgreementsHub, window.PMAgreementBuilder
// ════════════════════════════════════════════════════════════════════════
(function () {
  const { useState, useEffect, useCallback, useRef } = React;

  const today = () => new Date().toISOString().slice(0, 10);
  const fmt = (d) => d ? new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : '—';
  const ordinal = (n) => { const s = ['th', 'st', 'nd', 'rd'], v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); };
  function Spin() { return <span className="spin dark" />; }

  // ── standard clause set + placeholders (verbatim) ───────────────────────
  const STD_CLAUSES = [
    { id: 'c1', n: '1', title: 'Parties & property', state: 'auto', sub: 'landlord, tenant(s), address' },
    { id: 'c2', n: '2', title: 'Term & periodic basis', state: 'auto', sub: 'periodic · no end date (RRA 2026)' },
    { id: 'c3', n: '3', title: 'Rent & payment', state: 'auto', sub: 'amount · due day · method' },
    { id: 'c4', n: '4', title: 'Deposit', state: 'auto', sub: 'amount · scheme · 30-day clock' },
    { id: 'c5', n: '5', title: 'Repair obligations', state: 'standard', sub: 'L&T Act 1985 boilerplate' },
    { id: 'c8', n: '6', title: 'Pets', state: 'optional', sub: 'landlord 28-day decision (RRA-aligned)', defaultOn: true },
    { id: 'c9', n: '7', title: 'Smoking', state: 'optional', sub: 'prohibited indoors', defaultOn: true },
    { id: 'c10', n: '8', title: 'Subletting', state: 'optional', sub: 'permitted with landlord consent', defaultOn: false },
    { id: 'c12', n: '9', title: 'Right to Rent confirmation', state: 'auto', sub: 'from the onboarding check' },
    { id: 'c13', n: '10', title: 'Inventory & schedule', state: 'auto', sub: 'attaches after check-in inspection' },
    { id: 'c14', n: '11', title: 'Notices & service', state: 'standard', sub: 'email + post · s.196 LPA 1925' },
  ];
  const CUSTOM_CLAUSES = [
    { id: 'cc1', n: '12', title: 'Contents insurance', state: 'custom', sub: 'tenant maintains cover ≥ £15,000' },
    { id: 'cc2', n: '13', title: 'Cleaning standard', state: 'custom', sub: 'professional clean on exit' },
  ];
  const STD_PLACEHOLDERS = [
    { token: '[DATE]', source: 'today' }, { token: '[LANDLORD_NAME]', source: 'owner.name' },
    { token: '[TENANT_NAME]', source: 'lead.fullName' }, { token: '[ADDRESS]', source: 'property.full' },
    { token: '[MONTHLY_RENT]', source: 'tenancy.rentPcm' }, { token: '[PAY_DAY]', source: 'tenancy.payDay' },
    { token: '[DEPOSIT]', source: 'tenancy.depositAmount' }, { token: '[SCHEME]', source: 'tenancy.depositScheme' },
    { token: '[INSURANCE_MIN]', source: 'freetext', freeValue: '£15,000' },
  ];
  const LIBRARY_TEMPLATES = [
    { id: 'tpl-nrla', name: 'NRLA Standard AST', kind: 'library', tag: 'library', sub: 'periodic · model 2024 · RRA 2026 compliant', clauses: STD_CLAUSES, placeholders: STD_PLACEHOLDERS, rra: [{ ok: true, text: 'Maintained current with UK law' }] },
    { id: 'tpl-gov', name: 'Government model AST', kind: 'library', tag: 'library', sub: 'gov.uk · plain English · barebones', clauses: STD_CLAUSES.filter(c => c.state !== 'custom'), placeholders: STD_PLACEHOLDERS, rra: [{ ok: true, text: 'gov.uk model wording' }] },
    { id: 'tpl-company', name: 'Company let', kind: 'library', tag: 'library', sub: 'corporate tenant · directors as guarantors', clauses: STD_CLAUSES, placeholders: STD_PLACEHOLDERS, rra: [{ ok: true, text: 'Not an AST — company let terms' }] },
  ];
  const STARTER_TEMPLATE = {
    name: 'Our standard AST · periodic', kind: 'custom', tag: 'your template',
    sub: '13 clauses · 2 custom · starter', used_count: 0,
    clauses: [...STD_CLAUSES, ...CUSTOM_CLAUSES], placeholders: STD_PLACEHOLDERS,
    rra: [{ ok: true, text: 'No fixed-term clauses (RRA-safe)' }, { ok: true, text: 'No prohibited fees (Tenant Fees Act 2019)' }, { ok: true, text: 'Deposit ≤ 5 weeks\u2019 rent' }, { ok: true, text: 'Pets clause uses 28-day decision (RRA 2026)' }],
  };

  // ── mapping catalog + guesser (verbatim) ────────────────────────────────
  const MAP_SOURCES = [
    { group: 'Owner / Landlord', items: [['owner.name', 'Name'], ['owner.address', 'Address'], ['owner.phone', 'Phone'], ['owner.email', 'Email']] },
    { group: 'Tenant', items: [['lead.fullName', 'Lead tenant · name'], ['lead.email', 'Lead tenant · email'], ['lead.phone', 'Lead tenant · phone'], ['household.names', 'All tenants · names']] },
    { group: 'Property', items: [['property.full', 'Full address + postcode'], ['property.address', 'Address line'], ['property.postcode', 'Postcode'], ['property.type', 'Type (flat / house / HMO)'], ['property.beds', 'Bedrooms']] },
    { group: 'Rent & deposit', items: [['tenancy.rentPcm', 'Monthly rent £'], ['tenancy.payDay', 'Rent due day'], ['tenancy.startDate', 'Start date'], ['tenancy.depositAmount', 'Deposit £'], ['tenancy.depositScheme', 'Deposit scheme'], ['tenancy.term', 'Tenancy type']] },
    { group: 'Banking (rent collection)', items: [['bank.accountName', 'Account name'], ['bank.sortCode', 'Sort code'], ['bank.accountNumber', 'Account number']] },
    { group: 'Other', items: [['today', 'Today\u2019s date'], ['freetext', 'Free text (type a fixed value)']] },
  ];
  function guessSource(token) {
    const t = String(token || '').toUpperCase();
    const has = (...w) => w.some(x => t.indexOf(x) >= 0);
    if (has('LANDLORD', 'OWNER', 'LESSOR')) { if (has('ADDRESS')) return 'owner.address'; if (has('PHONE', 'TEL', 'MOBILE')) return 'owner.phone'; if (has('EMAIL')) return 'owner.email'; return 'owner.name'; }
    if (has('TENANT', 'LESSEE', 'OCCUP')) { if (has('EMAIL')) return 'lead.email'; if (has('PHONE', 'TEL', 'MOBILE')) return 'lead.phone'; return 'lead.fullName'; }
    if (has('SORT')) return 'bank.sortCode';
    if (has('ACCOUNT') && has('NUM', 'NO')) return 'bank.accountNumber';
    if (has('ACCOUNT') && has('NAME')) return 'bank.accountName';
    if (has('IBAN', 'BANK')) return 'bank.accountNumber';
    if (has('POSTCODE', 'POST_CODE')) return 'property.postcode';
    if (has('ADDRESS', 'PROPERTY', 'PREMISES', 'DEMISE')) return 'property.full';
    if (has('DEPOSIT')) return has('SCHEME') ? 'tenancy.depositScheme' : 'tenancy.depositAmount';
    if (has('SCHEME')) return 'tenancy.depositScheme';
    if (has('RENT')) return 'tenancy.rentPcm';
    if (has('PAY', 'DUE')) return 'tenancy.payDay';
    if (has('START', 'COMMENCE', 'BEGIN')) return 'tenancy.startDate';
    if (has('TERM', 'TYPE')) return 'tenancy.term';
    if (has('DATE')) return 'today';
    return '';
  }
  function resolveSource(source, freeValue, ctx) {
    const o = ctx.owner || {}, p = ctx.property || {}, l = ctx.lead || {}, d = ctx.draft || {}, hh = ctx.household || [];
    const money = v => '£' + (Number(v) || 0).toLocaleString();
    switch (source) {
      case 'owner.name': return o.name || '—';
      case 'owner.address': return o.address || '—';
      case 'owner.phone': return o.phone || '—';
      case 'owner.email': return o.email || '—';
      case 'lead.fullName': return l.full_name || '—';
      case 'lead.email': return l.email || '—';
      case 'lead.phone': return l.phone || '—';
      case 'household.names': return hh.map(m => m.full_name).filter(Boolean).join(', ') || '—';
      case 'property.address': return p.address || '—';
      case 'property.postcode': return p.postcode || '—';
      case 'property.full': return p.address ? p.address + (p.postcode ? ', ' + p.postcode : '') : '—';
      case 'property.type': return p.type ? String(p.type).toUpperCase() : '—';
      case 'property.beds': return p.beds != null ? String(p.beds) : '—';
      case 'tenancy.rentPcm': return money(d.rent_pcm) + ' pcm';
      case 'tenancy.payDay': return ordinal(Number(d.pay_day) || 1) + ' of the month';
      case 'tenancy.startDate': return d.start_date ? fmt(d.start_date) : '—';
      case 'tenancy.depositAmount': return Number(d.deposit_amount) ? money(d.deposit_amount) : 'none';
      case 'tenancy.depositScheme': return d.deposit_scheme || '—';
      case 'tenancy.term': return d.type === 'ast_fixed' ? 'fixed term' : 'periodic';
      case 'bank.accountName': return o.name || '—';
      case 'bank.sortCode': return o.bank_sort_code || '—';
      case 'bank.accountNumber': return o.bank_account || '—';
      case 'today': return fmt(d.start_date || today());
      case 'freetext': return freeValue || '—';
      default: return freeValue || '—';
    }
  }
  const rowMapped = (r) => !!r.source && (r.source !== 'freetext' || String(r.freeValue || '').trim().length > 0);
  const clauseTone = (s) => s === 'flagged' ? 'bad' : s === 'custom' ? 'warn' : s === 'optional' ? 'none' : 'ok';

  // ── docx → text (client-side; a .docx is a ZIP of XML) ──────────────────
  async function inflateRaw(bytes) {
    const ds = new DecompressionStream('deflate-raw');
    const stream = new Blob([bytes]).stream().pipeThrough(ds);
    return new Uint8Array(await new Response(stream).arrayBuffer());
  }
  async function docxToText(file) {
    try {
      if (typeof DecompressionStream === 'undefined') return '';
      const buf = new Uint8Array(await file.arrayBuffer());
      const dv = new DataView(buf.buffer);
      let eocd = -1;
      for (let i = buf.length - 22; i >= 0 && i > buf.length - 22 - 65536; i--) { if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; } }
      if (eocd < 0) return '';
      const cdOffset = dv.getUint32(eocd + 16, true), cdCount = dv.getUint16(eocd + 10, true);
      let p = cdOffset, target = null;
      for (let i = 0; i < cdCount; i++) {
        if (dv.getUint32(p, true) !== 0x02014b50) break;
        const method = dv.getUint16(p + 10, true), compSize = dv.getUint32(p + 20, true);
        const nameLen = dv.getUint16(p + 28, true), extraLen = dv.getUint16(p + 30, true), commentLen = dv.getUint16(p + 32, true);
        const localOffset = dv.getUint32(p + 42, true);
        const name = new TextDecoder().decode(buf.subarray(p + 46, p + 46 + nameLen));
        if (name === 'word/document.xml') { target = { method, compSize, localOffset }; break; }
        p += 46 + nameLen + extraLen + commentLen;
      }
      if (!target) return '';
      const lh = target.localOffset;
      if (dv.getUint32(lh, true) !== 0x04034b50) return '';
      const dataStart = lh + 30 + dv.getUint16(lh + 26, true) + dv.getUint16(lh + 28, true);
      const comp = buf.subarray(dataStart, dataStart + target.compSize);
      const xml = new TextDecoder().decode(target.method === 0 ? comp : await inflateRaw(comp));
      return xml.replace(/<\/w:p>/g, '\n').replace(/<w:tab[^>]*\/?>/g, '\t').replace(/<[^>]+>/g, '')
        .replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'")
        .replace(/\n{3,}/g, '\n\n').trim();
    } catch (e) { return ''; }
  }
  function readUpload(file) {
    return new Promise((resolve) => {
      const out = { name: file.name, mimeType: file.type, base64: '', text: '' };
      const isDocx = (file.type && file.type.indexOf('wordprocessingml.document') >= 0) || /\.docx$/i.test(file.name);
      const r = new FileReader();
      r.onload = async () => {
        const s = String(r.result || ''); const i = s.indexOf(',');
        out.base64 = i >= 0 ? s.slice(i + 1) : s;
        if (isDocx) out.text = await docxToText(file);
        else if (file.type && file.type.indexOf('text/') === 0) { try { out.text = await file.text(); } catch (e) {} }
        resolve(out);
      };
      r.onerror = () => resolve(out);
      r.readAsDataURL(file);
    });
  }
  const ensurePlaceholders = (obj) => {
    if (obj.placeholders && obj.placeholders.length) return obj.placeholders;
    const seen = {}, out = [];
    (obj.clauses || []).forEach(c => { (String((c && c.text) || '').match(/\[[A-Z0-9_]+\]/g) || []).forEach(tk => { if (!seen[tk]) { seen[tk] = 1; out.push(tk); } }); });
    return out;
  };

  // ── clause preview modal ────────────────────────────────────────────────
  function ClausePreview({ template, onClose }) {
    const clauses = template.clauses || [];
    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 620 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div><h2>{template.name}</h2><p className="modal-sub">{clauses.length} clauses · {clauses.filter(c => c.state === 'custom').length} custom</p></div><button className="x" onClick={onClose}>✕</button></div>
      <div style={{ padding: '0 24px 20px', maxHeight: '60vh', overflowY: 'auto' }}>
        {clauses.map((c, i) => <div className="ag-clause" key={c.id || i}>
          <span className="pmd-mono ag-n">{c.n || i + 1}</span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <strong>{c.title}</strong>
            <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{c.sub}</div>
            {c.text && <div className="ag-excerpt">{c.text}</div>}
          </div>
          <span className={'badge ' + clauseTone(c.state)}>{c.state}</span>
        </div>)}
      </div>
    </div></div>;
  }

  // ── upload-your-AST wizard (3 steps: file → name/type → extract + map) ──
  function TemplateUpload({ sb, remap, onClose, onSaved, toast }) {
    const [step, setStep] = useState(remap ? 3 : 1);
    const [file, setFile] = useState(null);
    const [name, setName] = useState(remap ? remap.name : '');
    const [kind, setKind] = useState('periodic');
    const [rows, setRows] = useState((remap && remap.placeholders && remap.placeholders.length)
      ? remap.placeholders.map(p => ({ token: p.token || p, source: p.source || guessSource(p.token || p), freeValue: p.freeValue || '' }))
      : STD_PLACEHOLDERS.map(p => ({ token: p.token, source: p.source, freeValue: p.freeValue || '' })));
    const [reading, setReading] = useState(false);
    const [extracting, setExtracting] = useState(false);
    const [extracted, setExtracted] = useState(remap ? { clauses: remap.clauses || [] } : null);
    const [extractError, setExtractError] = useState(null);
    const [busy, setBusy] = useState(false);
    const fileRef = useRef(null);

    const onFile = async (f) => {
      if (!f) return;
      setReading(true); setExtractError(null);
      if (!name) setName(f.name.replace(/\.(pdf|docx?|rtf|txt)$/i, '').replace(/[_-]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase()));
      setFile(await readUpload(f));
      setReading(false); setStep(2);
    };

    const goToMapping = async () => {
      setStep(3);
      if (remap || extracted || !file) return;
      setExtracting(true); setExtractError(null);
      try {
        const body = (file.mimeType === 'application/pdf' || (file.mimeType || '').startsWith('image/'))
          ? { base64: file.base64, mimeType: file.mimeType, kind: 'agreement' }
          : { text: file.text, kind: 'agreement' };
        if (!body.base64 && !(body.text && body.text.trim().length > 200)) throw new Error('no readable text');
        const { data, error } = await sb.functions.invoke('extract-document', { body });
        if (error || !data || data.error) throw new Error((data && data.error) || (error && error.message) || 'no result');
        const res = data.extracted || data;
        if (res && Array.isArray(res.clauses) && res.clauses.length) {
          res.placeholders = ensurePlaceholders(res);
          setExtracted(res);
          if (res.tenancyType) setKind(res.tenancyType === 'company' ? 'company' : res.tenancyType);
          if (res.placeholders.length) setRows(res.placeholders.map(tok => { const t = typeof tok === 'string' ? tok : tok.token; return { token: t, source: guessSource(t), freeValue: '' }; }));
        } else setExtractError('scaffold');
      } catch (e) { setExtractError(e.message || 'Could not read the document'); }
      finally { setExtracting(false); }
    };

    const allMapped = rows.every(rowMapped);
    const save = async () => {
      const hmo = kind === 'hmo';
      let clauses;
      if (extracted && Array.isArray(extracted.clauses) && extracted.clauses.length) {
        clauses = extracted.clauses.map((c, i) => ({
          id: c.id || ('xc' + i), n: c.n || String(i + 1), title: c.title || ('Clause ' + (i + 1)),
          state: ['auto', 'standard', 'custom', 'optional'].includes(c.state) ? c.state : 'standard',
          sub: c.sub || '', text: c.text || '', defaultOn: c.state === 'optional' ? true : undefined,
        }));
      } else {
        clauses = [...STD_CLAUSES, ...CUSTOM_CLAUSES];
        if (hmo) clauses.push({ id: 'cc3', n: '14', title: 'HMO household & licensing', state: 'custom', sub: 'single related household · licence ref' });
      }
      const fromDoc = !!(extracted && extracted.clauses && extracted.clauses.length && !remap);
      const patch = {
        name: name.trim() || (file ? file.name : 'My AST'), kind: 'custom', tag: 'your template',
        sub: clauses.length + ' clauses' + (fromDoc ? ' · read from your document' : remap ? '' : ' · scaffold') + ' · ' + (kind === 'fixed' ? 'fixed term' : kind === 'hmo' ? 'HMO household' : kind === 'company' ? 'company let' : 'periodic'),
        clauses,
        placeholders: rows.map(r => ({ token: r.token, source: r.source, freeValue: r.source === 'freetext' ? (r.freeValue || '') : '' })),
        rra: [
          { ok: kind !== 'fixed', text: kind === 'fixed' ? 'Fixed term — review against RRA 2026' : 'No fixed-term clauses (RRA-safe)' },
          { ok: true, text: 'No prohibited fees (Tenant Fees Act 2019)' },
          { ok: true, text: 'Deposit ≤ 5 weeks\u2019 rent' },
        ],
      };
      setBusy(true);
      const q = remap ? sb.from('templates').update(patch).eq('id', remap.id) : sb.from('templates').insert(patch);
      const { error } = await q;
      setBusy(false);
      if (error) return toast(error.message);
      toast(remap ? 'Template updated' : 'Template saved'); onSaved();
    };

    const customCount = extracted && extracted.clauses ? extracted.clauses.filter(c => c.state === 'custom').length : 0;

    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 700 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div>
        <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>{remap ? 'REMAP · ' + remap.name : 'UPLOAD YOUR TEMPLATE'} · STEP {step} OF 3</div>
        <h2>{step === 1 ? 'Upload your AST' : step === 2 ? 'Name & type' : extracting ? 'Reading your agreement…' : 'Review & map'}</h2>
        {step === 1 && <p className="modal-sub">Drop your own tenancy agreement — PDF or Word (.docx). We read its actual clauses and variables so every new tenancy auto-fills from your records, keeping your wording.</p>}
        {step === 2 && <p className="modal-sub">Uploaded <b>{file && file.name}</b>. Name it and tell us the tenancy type — we read the clauses next.</p>}
      </div><button className="x" onClick={onClose}>✕</button></div>

      {step === 1 && <div style={{ padding: '0 24px' }}>
        <input ref={fileRef} type="file" accept=".pdf,.doc,.docx,.rtf,.txt" style={{ display: 'none' }} onChange={e => onFile(e.target.files[0])} />
        <div className="dropzone" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, padding: '30px 14px', cursor: 'pointer' }}
          onClick={() => fileRef.current && fileRef.current.click()} onDragOver={e => e.preventDefault()} onDrop={e => { e.preventDefault(); onFile(e.dataTransfer.files[0]); }}>
          <span style={{ fontSize: 22 }}>{reading ? '⏳' : '↑'}</span>
          <span style={{ fontWeight: 600 }}>{reading ? 'Reading file…' : 'Drop your AST here, or click to browse'}</span>
          <span className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>PDF · DOCX · TXT</span>
        </div>
        <div className="modal-foot" style={{ padding: '16px 0 24px' }}>
          <span className="foot-note">Your wording is preserved — clauses are read once and saved as a reusable template.</span>
          <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
        </div>
      </div>}

      {step === 2 && <React.Fragment>
        <div className="modal-form">
          <div className="field"><label>Template name</label><input value={name} autoFocus onChange={e => setName(e.target.value)} /></div>
          <div className="field"><label>Tenancy type</label><select value={kind} onChange={e => setKind(e.target.value)}>
            <option value="periodic">AST · periodic (RRA 2026)</option><option value="fixed">AST · fixed term (legacy)</option>
            <option value="hmo">HMO household</option><option value="company">Company let</option>
          </select></div>
        </div>
        <div className="modal-foot" style={{ justifyContent: 'space-between' }}>
          <button className="btn btn-ghost" onClick={() => setStep(1)}>← Back</button>
          <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!name.trim()} onClick={goToMapping}>Read clauses &amp; map →</button>
        </div>
      </React.Fragment>}

      {step === 3 && (extracting ? <div style={{ textAlign: 'center', padding: '26px 0 34px' }}><Spin /><div style={{ marginTop: 12, fontWeight: 600 }}>Extracting the real clause wording and variables…</div></div>
        : <React.Fragment>
          <div style={{ padding: '0 24px' }}>
            {extracted && extracted.clauses && extracted.clauses.length ? <p className="modal-sub">Read <b>{extracted.clauses.length} clauses</b>{customCount ? ' (' + customCount + ' bespoke)' : ''}{extracted.confidence ? ' · confidence ' + extracted.confidence : ''}. Your wording is preserved. Confirm where the {rows.length} variables pull from.</p>
              : <p className="modal-sub">{extractError && extractError !== 'scaffold' ? 'Couldn\u2019t read clauses automatically (' + extractError + '). ' : ''}Saving with the standard clause scaffold — confirm the {rows.length} variables below.</p>}
            {extracted && extracted.clauses && extracted.clauses.length > 0 && <div style={{ maxHeight: 170, overflowY: 'auto', border: '1px solid var(--line)', borderRadius: 8, padding: '4px 12px', marginBottom: 12 }}>
              {extracted.clauses.slice(0, 6).map((c, i) => <div className="ag-clause" key={i}>
                <span className="pmd-mono ag-n">{c.n || i + 1}</span>
                <div style={{ flex: 1, minWidth: 0 }}><strong>{c.title}</strong>{c.text && <div className="ag-excerpt">{String(c.text).slice(0, 110)}{String(c.text).length > 110 ? '…' : ''}</div>}</div>
                <span className={'badge ' + clauseTone(c.state)}>{c.state}</span>
              </div>)}
              {extracted.clauses.length > 6 && <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)', padding: '6px 0 8px 30px' }}>+ {extracted.clauses.length - 6} more clauses</div>}
            </div>}
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
              <span style={{ fontSize: 12.5, color: 'var(--ink-faint)' }}>Point each variable at a data field, or <b>Free text</b> for a fixed value.</span>
              <button className="btn btn-ghost btn-sm" onClick={() => setRows(rows.map(r => ({ ...r, source: guessSource(r.token) || r.source })))}>Auto-suggest all</button>
            </div>
            <div style={{ maxHeight: 230, overflowY: 'auto', border: '1px solid var(--line)', borderRadius: 8 }}>
              <table className="prop-table"><tbody>
                {rows.map((r, idx) => <tr key={r.token + idx}>
                  <td><span className="ag-token">{r.token}</span></td>
                  <td style={{ color: 'var(--ink-faint)' }}>→</td>
                  <td><div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
                    <select className="pm-input" value={r.source} onChange={e => setRows(rows.map((x, i) => i === idx ? { ...x, source: e.target.value } : x))}>
                      <option value="">— choose source —</option>
                      {MAP_SOURCES.map(g => <optgroup key={g.group} label={g.group}>{g.items.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</optgroup>)}
                    </select>
                    {r.source === 'freetext' && <input className="pm-input" placeholder="Fixed value" value={r.freeValue || ''} onChange={e => setRows(rows.map((x, i) => i === idx ? { ...x, freeValue: e.target.value } : x))} />}
                  </div></td>
                  <td><span className={'hdot ' + (rowMapped(r) ? 'ok' : 'warn')}></span></td>
                </tr>)}
              </tbody></table>
            </div>
          </div>
          <div className="modal-foot" style={{ justifyContent: 'space-between' }}>
            <button className="btn btn-ghost" onClick={() => setStep(2)} disabled={!!remap}>← Back</button>
            <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!allMapped || busy} onClick={save}>{busy ? <Spin /> : allMapped ? (remap ? 'Save changes' : 'Save as my template') : 'Map all variables first'}</button>
          </div>
        </React.Fragment>)}
    </div></div>;
  }

  // ── agreements hub ──────────────────────────────────────────────────────
  function AgreementsHub({ sb, account, toast }) {
    const [templates, setTemplates] = useState(null);
    const [upload, setUpload] = useState(null);   // null | {} | {remap}
    const [preview, setPreview] = useState(null);
    const [renaming, setRenaming] = useState(null);
    const [busy, setBusy] = useState(false);

    const load = useCallback(async () => {
      const { data } = await sb.from('templates').select('*').order('created_at', { ascending: false });
      setTemplates(data || []);
    }, [sb]);
    useEffect(() => { load(); }, [load]);

    if (templates === null) return <div className="card card-pad"><Spin /></div>;

    const custom = templates.filter(t => t.kind === 'custom');
    const addStarter = async () => {
      setBusy(true);
      const { error } = await sb.from('templates').insert(STARTER_TEMPLATE);
      setBusy(false);
      if (error) return toast(error.message);
      toast('Starter template added'); load();
    };
    const renameTo = async (t, nm) => {
      setRenaming(null);
      nm = (nm || '').trim();
      if (!nm || nm === t.name) return;
      const { error } = await sb.from('templates').update({ name: nm }).eq('id', t.id);
      if (error) toast(error.message); else { toast('Renamed'); load(); }
    };
    const remove = async (t) => {
      if (!confirm('Delete template "' + t.name + '"?')) return;
      const { error } = await sb.from('templates').delete().eq('id', t.id);
      if (error) toast(error.message); else { toast('Template deleted'); load(); }
    };

    const card = (t, isDb) => <div key={t.id} className={'card ag-tpl' + (t.kind === 'custom' ? ' custom' : '')}>
      <div className="ag-tpl-top"><span className={'badge ' + (t.kind === 'custom' ? 'warn' : 'none')}>{t.tag || t.kind}</span><span className="hdot ok"></span></div>
      {renaming === t.id ? <input className="pm-input" style={{ width: '100%' }} autoFocus defaultValue={t.name}
        onBlur={e => renameTo(t, e.target.value)} onKeyDown={e => { if (e.key === 'Enter') renameTo(t, e.target.value); if (e.key === 'Escape') setRenaming(null); }} />
        : <h3 style={{ fontSize: 15.5, display: 'flex', gap: 6, alignItems: 'baseline' }}>{t.name}{isDb && <button className="linkbtn" style={{ fontSize: 12 }} title="Rename" onClick={() => setRenaming(t.id)}>✎</button>}</h3>}
      <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)', margin: '4px 0 10px' }}>{t.sub}{t.used_count ? ' · used ' + t.used_count + '\u00d7' : ''}</div>
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
        <button className="btn btn-ghost btn-sm" onClick={() => setPreview(t)}>Preview clauses</button>
        {isDb && <button className="btn btn-ghost btn-sm" onClick={() => setUpload({ remap: t })}>Remap</button>}
        {isDb && <button className="btn btn-ghost btn-sm" style={{ color: 'var(--red)', padding: '8px 10px' }} onClick={() => remove(t)}>✕</button>}
      </div>
    </div>;

    const rraSource = custom[0] || STARTER_TEMPLATE;

    return <React.Fragment>
      <div className="pagehead" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 14, flexWrap: 'wrap' }}>
        <div><h1>Tenancy agreements</h1><p>Your saved templates pre-fill from property + tenant records on every let. Library templates are kept current with UK law.</p></div>
        <button className="btn btn-primary btn-sm" onClick={() => setUpload({})}>+ Upload your AST</button>
      </div>

      <div className="navgroup-label" style={{ padding: '0 0 8px' }}>Your templates · {custom.length}</div>
      {custom.length === 0 ? <div className="card" style={{ marginBottom: 18 }}><div className="empty" style={{ padding: '22px 16px' }}>
        <div className="ico">📑</div><h3>No saved templates yet</h3><p>Upload your own AST (we read its real clauses), or start from the standard scaffold.</p>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
          <button className="btn btn-ghost btn-sm" disabled={busy} onClick={addStarter}>{busy ? <Spin /> : 'Add starter template'}</button>
          <button className="btn btn-primary btn-sm" onClick={() => setUpload({})}>+ Upload your AST</button>
        </div>
      </div></div>
        : <div className="ag-grid" style={{ marginBottom: 18 }}>{custom.map(t => card(t, true))}</div>}

      <div className="navgroup-label" style={{ padding: '0 0 8px' }}>Library templates · {LIBRARY_TEMPLATES.length}</div>
      <div className="ag-grid">{LIBRARY_TEMPLATES.map(t => card(t, false))}</div>

      <div className="grid2" style={{ marginTop: 18 }}>
        <div className="card card-pad">
          <h3 style={{ fontSize: 15, marginBottom: 10 }}>How "your template" works</h3>
          <div className="ag-how">
            {[['1', 'Upload', 'your existing AST · PDF or Word'], ['2', 'Read', 'we extract your real clauses'], ['3', 'Map', 'placeholders auto-fill from records'], ['4', 'Reuse', 'every new tenancy starts pre-filled']].map(([n, h, s]) =>
              <div key={n} className="ag-how-step"><span className="ag-how-n">{n}</span><div><strong>{h}</strong><div className="pmd-mono" style={{ fontSize: 10.5, color: 'var(--ink-faint)' }}>{s}</div></div></div>)}
          </div>
        </div>
        <div className="card card-pad" style={{ background: 'var(--ok-soft)' }}>
          <h3 style={{ fontSize: 15, marginBottom: 10 }}>✓ RRA 2026 ready</h3>
          <ul style={{ margin: 0, paddingLeft: 4, listStyle: 'none', fontSize: 13.5, display: 'flex', flexDirection: 'column', gap: 6 }}>
            {(rraSource.rra || []).map((r, i) => <li key={i} style={{ color: r.ok ? 'var(--ink-soft)' : 'var(--amber)' }}>{r.ok ? '✓' : '⚠'} {r.text}</li>)}
          </ul>
        </div>
      </div>

      {preview && <ClausePreview template={preview} onClose={() => setPreview(null)} />}
      {upload && <TemplateUpload sb={sb} remap={upload.remap} onClose={() => setUpload(null)} onSaved={() => { setUpload(null); load(); }} toast={toast} />}
    </React.Fragment>;
  }

  // ── agreement builder: template → clauses → live draft → e-sign ─────────
  function AgreementBuilder({ sb, account, property, tenancy, onClose, onDone, toast }) {
    const [step, setStep] = useState(1);
    const [templates, setTemplates] = useState(null);
    const [tplId, setTplId] = useState(null);
    const [optional, setOptional] = useState({});
    const [household, setHousehold] = useState([]);
    const [owner, setOwner] = useState(null);
    const [signName, setSignName] = useState('');
    const [busy, setBusy] = useState(false);

    useEffect(() => { (async () => {
      const [tp, hm, ow] = await Promise.all([
        sb.from('templates').select('*').order('created_at', { ascending: false }),
        sb.from('tenancy_members').select('tenant_id,relation,position').eq('tenancy_id', tenancy.id),
        property.primary_owner_id ? sb.from('owners').select('*').eq('id', property.primary_owner_id).maybeSingle() : Promise.resolve({ data: null }),
      ]);
      const all = [...(tp.data || []), ...LIBRARY_TEMPLATES];
      setTemplates(all);
      setTplId((all[0] || {}).id || null);
      setOwner(ow.data || null);
      const ids = (hm.data || []).map(m => m.tenant_id);
      if (ids.length) { const tn = await sb.from('tenants').select('*').in('id', ids); const byId = Object.fromEntries((tn.data || []).map(t => [t.id, t])); setHousehold((hm.data || []).sort((a, b) => (a.position || 0) - (b.position || 0)).map(m => byId[m.tenant_id]).filter(Boolean)); }
    })(); }, []);

    if (templates === null) return <div className="modal-scrim"><div className="modal" style={{ maxWidth: 660, padding: 30, textAlign: 'center' }}><Spin /></div></div>;

    const tmpl = templates.find(t => t.id === tplId) || templates[0];
    const lead = household[0] || {};
    const ctx = { owner, property, lead, draft: tenancy, household };
    const fills = {};
    (tmpl && tmpl.placeholders || []).forEach(ph => {
      const tok = typeof ph === 'string' ? ph : ph.token;
      fills[tok] = resolveSource(typeof ph === 'string' ? guessSource(ph) : ph.source, ph.freeValue, ctx);
    });
    const clauses = (tmpl && tmpl.clauses || []).filter(c => c.state !== 'optional' || (optional[c.id] !== undefined ? optional[c.id] : c.defaultOn !== false));
    const fillText = (txt) => String(txt || '').replace(/\[[A-Z0-9_]+\]/g, tk => fills[tk] || tk);

    const sign = async () => {
      if (!signName.trim()) return;
      setBusy(true);
      try {
        // build the signed agreement document (self-contained HTML)
        const docHtml = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Tenancy agreement · ' + property.address + '</title>'
          + '<style>body{font-family:Georgia,serif;max-width:720px;margin:40px auto;padding:0 20px;color:#1a1815;line-height:1.6}h1{font-size:22px}h2{font-size:15px;margin:22px 0 4px}.meta{color:#777;font-size:13px}.sig{margin-top:40px;border-top:1px solid #ccc;padding-top:16px}</style></head><body>'
          + '<h1>Tenancy agreement</h1><div class="meta">' + property.address + (property.postcode ? ', ' + property.postcode : '') + ' · generated ' + fmt(today()) + ' · template: ' + (tmpl.name || '') + '</div>'
          + clauses.map((c, i) => '<h2>' + (c.n || i + 1) + '. ' + c.title + '</h2><p>' + (fillText(c.text) || c.sub || '') + '</p>').join('')
          + '<div class="sig"><strong>Signed (landlord):</strong> ' + signName.trim() + ' · ' + fmt(today()) + '<br><strong>Lead tenant:</strong> ' + (lead.full_name || '—') + '</div></body></html>';
        const path = account.id + '/agreements/' + Date.now() + '-agreement.html';
        const up = await sb.storage.from('documents').upload(path, new Blob([docHtml], { type: 'text/html' }), { contentType: 'text/html' });
        if (up.error) throw new Error(up.error.message);
        const { data: doc, error: dErr } = await sb.from('documents').insert({
          filename: 'Tenancy agreement · ' + property.address + '.html', storage_path: path, mime_type: 'text/html',
          property_id: property.id, category: 'letter',
        }).select('id').single();
        if (dErr) throw new Error(dErr.message);
        await sb.from('tenancies').update({ agreement_doc_id: doc.id, template_id: LIBRARY_TEMPLATES.some(l => l.id === tmpl.id) ? null : tmpl.id }).eq('id', tenancy.id);
        if (!LIBRARY_TEMPLATES.some(l => l.id === tmpl.id)) await sb.from('templates').update({ used_count: (tmpl.used_count || 0) + 1 }).eq('id', tmpl.id);
        toast('Agreement signed & filed');
        onDone();
      } catch (e) { toast(e.message || String(e)); }
      finally { setBusy(false); }
    };

    return <div className="modal-scrim" onClick={onClose}><div className="modal" style={{ maxWidth: 700 }} onClick={e => e.stopPropagation()}>
      <div className="modal-head"><div>
        <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--brand-deep)' }}>AGREEMENT · STEP {step} OF 3</div>
        <h2>{step === 1 ? 'Choose a template' : step === 2 ? 'Review clauses & draft' : 'Sign'}</h2>
        <p className="modal-sub">{property.address} · {lead.full_name || 'household'}</p>
      </div><button className="x" onClick={onClose}>✕</button></div>

      {step === 1 && <div style={{ padding: '0 24px', display: 'flex', flexDirection: 'column', gap: 8 }}>
        {templates.map(t => <label key={t.id} className={'gate-row' + (tplId === t.id ? ' on' : '')}>
          <input type="radio" name="tpl" checked={tplId === t.id} onChange={() => setTplId(t.id)} />
          <div><strong>{t.name}</strong><div className="s">{t.sub}{t.used_count ? ' · used ' + t.used_count + '\u00d7' : ''}</div></div>
        </label>)}
      </div>}

      {step === 2 && <div style={{ padding: '0 24px', maxHeight: '52vh', overflowY: 'auto' }}>
        {(tmpl.clauses || []).map((c, i) => {
          const on = c.state !== 'optional' || (optional[c.id] !== undefined ? optional[c.id] : c.defaultOn !== false);
          return <div className={'ag-clause' + (on ? '' : ' off')} key={c.id || i}>
            <span className="pmd-mono ag-n">{c.n || i + 1}</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <strong>{c.title}</strong>
              <div className="pmd-mono" style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{c.sub}</div>
              {c.text && on && <div className="ag-excerpt filled">{fillText(c.text)}</div>}
            </div>
            {c.state === 'optional' ? <label className="chk" style={{ fontSize: 12 }}><input type="checkbox" checked={on} onChange={e => setOptional({ ...optional, [c.id]: e.target.checked })} /> include</label>
              : <span className={'badge ' + clauseTone(c.state)}>{c.state}</span>}
          </div>;
        })}
      </div>}

      {step === 3 && <div style={{ padding: '0 24px' }}>
        <div className="alert alert-info"><span className="ic">✍</span><div>{clauses.length} clauses, filled from your records — <b>{lead.full_name || 'tenant'}</b>, {fills['[MONTHLY_RENT]'] || ''}, {fills['[ADDRESS]'] || property.address}. Signing files the agreement to this tenancy.</div></div>
        <div className="field" style={{ maxWidth: 320 }}><label>Sign — type your full name (landlord)</label><input value={signName} autoFocus placeholder={owner ? owner.name : 'Full name'} onChange={e => setSignName(e.target.value)} /></div>
        {signName.trim() && <div className="ag-signature">{signName}</div>}
      </div>}

      <div className="modal-foot" style={{ justifyContent: 'space-between' }}>
        <button className="btn btn-ghost" onClick={() => step > 1 ? setStep(step - 1) : onClose()}>{step > 1 ? '← Back' : 'Cancel'}</button>
        {step < 3 ? <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!tmpl} onClick={() => setStep(step + 1)}>Continue →</button>
          : <button className="btn btn-primary" style={{ width: 'auto' }} disabled={!signName.trim() || busy} onClick={sign}>{busy ? <Spin /> : 'Sign & file agreement ✓'}</button>}
      </div>
    </div></div>;
  }

  window.PMAgreementsHub = AgreementsHub;
  window.PMAgreementBuilder = AgreementBuilder;
  // shared with the onboarding wizard — library templates + token resolution
  window.PMAgreementLib = { libraryTemplates: LIBRARY_TEMPLATES, stdClauses: STD_CLAUSES, resolveSource, guessSource };
})();
