/* global React, Icon, initials, fmtDate, fmtPhone, relTime, api, toast, useApi, Drawer */

/* ====================================================================
   Documents — REAL DATA from /api/documents
   ==================================================================== */
const DocumentsView = () => {
  const [q, setQ] = React.useState("");
  const [type, setType] = React.useState("all");
  const [openId, setOpenId] = React.useState(null);
  // Bumping `refreshKey` on a successful upload re-runs the useApi
  // fetcher (it's part of the dep array). The browser-side cache
  // invalidation in api.uploadDocument handles the cache layer; this
  // makes the view re-fetch immediately rather than waiting for the
  // SSE-driven refresh to land.
  const [refreshKey, setRefreshKey] = React.useState(0);
  const { data: documents, loading } = useApi(
    () => api.documents(type !== 'all' ? type : null, q || null),
    [type, q, refreshKey]
  );

  // SSE: when the bridge finishes vision classification on an uploaded
  // doc, refresh the list so the type/vendor/amount populate without a
  // manual refresh. Phone uploads can take 10-20s for the vision step
  // — without this, you see the file appear as "pending" and have to
  // click the page to see it become "Receipt — Home Depot — $89.99".
  React.useEffect(() => {
    let es = null;
    let reconnectTimer = null;
    const open = () => {
      es = new EventSource('/api/live');
      es.onmessage = (ev) => {
        let m;
        try { m = JSON.parse(ev.data); } catch { return; }
        if (m.type === 'document_classified' || m.type === 'document_ingested') {
          api.invalidate('/api/documents');
          setRefreshKey(k => k + 1);
        }
      };
      es.onerror = () => {
        if (es) { es.close(); es = null; }
        reconnectTimer = setTimeout(open, 3000);
      };
    };
    open();
    return () => {
      if (es) es.close();
      if (reconnectTimer) clearTimeout(reconnectTimer);
    };
  }, []);

  // Hidden file-picker input — the visible button calls .click() on it
  // so the styling matches the rest of the topbar without using a raw
  // <input type="file"> which is hard to theme. accept= keeps the OS
  // file picker pre-filtered to formats the bridge actually accepts.
  const fileInputRef = React.useRef(null);
  const [uploading, setUploading] = React.useState(false);
  const onUploadClick = () => fileInputRef.current?.click();
  const onFilePicked = async (e) => {
    const file = e.target.files?.[0];
    e.target.value = "";  // allow re-uploading the same filename later
    if (!file) return;
    setUploading(true);
    try {
      await api.uploadDocument(file);
      // Vision classification runs in the background; we'll get a
      // `document_classified` SSE event when it lands and the list
      // refreshes automatically. For now the row shows up as pending.
      toast("Uploaded — classifying…");
      setRefreshKey(k => k + 1);
    } catch (err) {
      toast(`Upload failed: ${err.message}`);
    } finally {
      setUploading(false);
    }
  };

  const types = [
    ["all","All"], ["receipt","Receipts"], ["invoice","Invoices"],
    ["equipment","Equipment"], ["pool_issue","Pool issues"],
    ["chemical_log","Chem logs"], ["business_doc","Business"],
  ];

  const docList = documents || [];

  const typeChip = (t) => ({
    receipt: "sage", invoice: "sun", equipment: "clay",
    pool_issue: "alert", chemical_log: "pool", business_doc: "ink"
  }[t] || "");
  const typeLabel = (t) => ({
    receipt: "Receipt", invoice: "Invoice", equipment: "Equipment",
    pool_issue: "Pool issue", chemical_log: "Chem log", business_doc: "Business doc", other: "Other"
  }[t] || t || "Other");

  // Build a meaningful display name from the classified fields.
  // Priority order:
  //   0. <Title>                           → "CVS receipt — chlorine strips ($23.40)"
  //                                          (the new gemma-4 title field; preferred when present)
  //   1. <Vendor> <Type> · <ShortDate>     → "Home Depot Receipt · Apr 29"
  //   2. <Vendor> <Type>                   → "Home Depot Receipt"
  //   3. <Type> · <ShortDate>              → "Receipt · Apr 29"
  //   4. <Summary>                         → "Pump replacement service ticket"
  //   5. <Type>                            → "Receipt"
  //   6. <Filename>                        → "IMG_5432.HEIC" (last-resort fallback)
  // Why this order: the classifier-generated title is the highest-quality
  // label when available — it includes the most-identifying detail (vendor,
  // model, dollar amount, customer). Older rows pre-date the title field
  // and fall through to the legacy vendor-+-type composer.
  const docDisplayName = (d) => {
    if (!d) return 'Document';
    if (d.title && d.title.trim()) return d.title.trim();
    const type = typeLabel(d.document_type);
    const isGenericType = !d.document_type || d.document_type === 'other' || d.document_type === 'pending';
    // Parse YYYY-MM-DD as a LOCAL date, not UTC. Otherwise the
    // timezone offset shifts "2026-04-29" to "Apr 28" in any negative-UTC
    // zone (e.g. EST). doc_date is a calendar date with no time, so
    // local interpretation is correct.
    let shortDate = '';
    if (d.doc_date) {
      const m = d.doc_date.match(/^(\d{4})-(\d{2})-(\d{2})/);
      if (m) {
        try {
          const dt = new Date(+m[1], +m[2] - 1, +m[3]);
          shortDate = dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
        } catch {}
      }
    }
    if (d.vendor && !isGenericType && shortDate) return `${d.vendor} ${type} · ${shortDate}`;
    if (d.vendor && !isGenericType)              return `${d.vendor} ${type}`;
    if (d.vendor && shortDate)                   return `${d.vendor} · ${shortDate}`;
    if (d.vendor)                                 return d.vendor;
    if (!isGenericType && shortDate)              return `${type} · ${shortDate}`;
    if (d.summary)                                return d.summary;
    if (!isGenericType)                           return type;
    // Last-resort fallback: filename. But filter out the synthetic
    // bridge-upload prefix and bare UUIDs (OpenClaw inbound media_ids)
    // because those names tell the user nothing — better to show the
    // upload date as a hint of when it came in.
    const fname = d.filename || '';
    const synthetic = /^hub-upload-[0-9a-f]{32,}/i.test(fname) || /^[0-9a-f-]{32,}\./i.test(fname);
    if (fname && !synthetic) return fname;
    if (d.created_at) {
      try {
        const dt = new Date(d.created_at);
        if (!isNaN(dt)) return `Document · ${dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
      } catch {}
    }
    return 'Document';
  };

  const open = docList.find(d => d.id === openId);

  return (
    <div>
      <div className="page-head">
        <div>
          <h1>Documents</h1>
          <div className="subtitle">Receipts, invoices, photos and equipment manuals — auto-categorized as they come in.</div>
        </div>
        <button
          className="btn primary"
          onClick={onUploadClick}
          disabled={uploading}
        >
          <Icon name="plus" size={16} /> {uploading ? "Uploading…" : "Upload"}
        </button>
        <input
          ref={fileInputRef}
          type="file"
          accept="image/*,application/pdf,.heic,.heif"
          style={{ display: "none" }}
          onChange={onFilePicked}
        />
      </div>

      <div className="filters-bar">
        <div className="search-inline">
          <Icon name="search" size={16} />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search summary, vendor, filename…" />
        </div>
        <div style={{display:"flex", gap:6, flexWrap:"wrap"}}>
          {types.map(([k, label]) => (
            <button key={k} className={`filter-toggle ${type === k ? "active" : ""}`} onClick={() => setType(k)}>
              {label}
            </button>
          ))}
        </div>
        <span className="results-count">{loading ? '…' : `${docList.length} documents`}</span>
      </div>

      <div className="doc-grid">
        {docList.map(d => (
          <div key={d.id} className="doc-card" onClick={() => setOpenId(d.id)}>
            <div className="img-placeholder">
              {d.content_type?.startsWith('image/') ?
                <img src={api.documentFileUrl(d.id)} alt={docDisplayName(d)} style={{width:'100%', height:'100%', objectFit:'cover', borderRadius:'var(--r-sm)'}} /> :
                docDisplayName(d)
              }
            </div>
            <div className="doc-body" title={d.description || d.summary || ''}>
              <div style={{display:"flex", gap:6, flexWrap:"wrap"}}>
                <span className={`chip ${typeChip(d.document_type)}`}>{typeLabel(d.document_type)}</span>
                {d.vendor && <span className="chip">{d.vendor}</span>}
              </div>
              <div className="doc-summary">{docDisplayName(d)}</div>
              {/* description is the new gemma-4 paragraph; fall back to legacy summary
                  for documents classified before the upgrade. CSS clamps to 2 lines so
                  cards stay uniform — the full text is in the title/tooltip + drawer. */}
              {(d.description || d.summary) && (
                <div className="doc-description" style={{
                  fontSize:12, color:'var(--ink-2)', lineHeight:1.4, marginTop:4,
                  display:'-webkit-box', WebkitLineClamp:2, WebkitBoxOrient:'vertical',
                  overflow:'hidden',
                }}>
                  {d.description || d.summary}
                </div>
              )}
              <div className="doc-meta">
                <span>{relTime(d.created_at)}</span>
                {d.amount_cents != null && <span className="doc-amount">${(d.amount_cents / 100).toFixed(2)}</span>}
              </div>
            </div>
          </div>
        ))}
        {docList.length === 0 && !loading && <div className="empty-state card">No documents found.</div>}
      </div>

      {open && (
        <Drawer
          // Drawer title: meaningful name from vendor + type + date
          // (or vendor + amount when both present), falling back to
          // summary, then type label, then filename.
          title={open.vendor && open.amount_cents != null
            ? `${open.vendor} — $${(open.amount_cents / 100).toFixed(2)}`
            : docDisplayName(open)}
          subtitle={`${typeLabel(open.document_type)} · ${relTime(open.created_at)}`} onClose={() => setOpenId(null)}
          actions={[{ label: "Download", primary: true, onClick: () => window.open(api.documentFileUrl(open.id), '_blank') }]}
        >
          {open.content_type?.startsWith('image/') ?
            <img src={api.documentFileUrl(open.id)} alt={open.summary} style={{width:'100%', borderRadius:'var(--r)', marginBottom:16}} /> :
            <div className="img-placeholder" style={{aspectRatio:"4/3", borderRadius:"var(--r)", marginBottom:16}}>
              {open.filename || 'no preview available'}
            </div>
          }
          <div className="intake-grid">
            <div className="intake-field"><div className="if-label">Type</div><div className="if-value">{typeLabel(open.document_type)}</div></div>
            <div className="intake-field"><div className="if-label">Vendor</div><div className={`if-value ${open.vendor ? "" : "missing"}`}>{open.vendor || "—"}</div></div>
            <div className="intake-field"><div className="if-label">Amount</div><div className={`if-value ${open.amount_cents != null ? "" : "missing"}`}>{open.amount_cents != null ? `$${(open.amount_cents / 100).toFixed(2)}` : "—"}</div></div>
            <div className="intake-field"><div className="if-label">Date</div><div className={`if-value ${open.doc_date ? "" : "missing"}`}>{open.doc_date || "—"}</div></div>
            <div className="intake-field"><div className="if-label">From</div><div className="if-value">{open.phone ? fmtPhone(open.phone) : "—"}</div></div>
            <div className="intake-field"><div className="if-label">Received</div><div className="if-value">{relTime(open.created_at)}</div></div>
          </div>
          {/* Description: the new gemma-4 paragraph — explains what the document IS FOR
              in pool-business context. Falls back to the legacy summary line for documents
              classified before the title/description upgrade. */}
          {(open.description || open.summary) && (
            <div style={{fontSize:14, lineHeight:1.5, color:"var(--ink-2)", marginTop:12}}>
              {open.description || open.summary}
            </div>
          )}
          {/* If we have BOTH (description preferred, summary legacy), show the legacy
              summary in muted form below so audit trail is preserved without crowding. */}
          {open.description && open.summary && open.summary !== open.description && (
            <div style={{fontSize:12, color:'var(--ink-3)', marginTop:8, fontStyle:'italic'}}>
              <span style={{opacity:0.7}}>Legacy summary: </span>{open.summary}
            </div>
          )}
        </Drawer>
      )}
    </div>
  );
};

/* ====================================================================
   Fleet — REAL DATA from /api/vehicles
   ==================================================================== */
const FleetView = () => {
  const { data: vehicles, loading } = useApi(() => api.vehicles(), []);
  const vList = vehicles || [];

  const dueState = (iso) => {
    if (!iso) return { cls:"", days:null, text:"—" };
    const d = new Date(iso);
    const today = new Date(); today.setHours(0,0,0,0);
    const days = Math.round((d - today) / 86400e3);
    if (days < 0) return { cls:"overdue", days, text:`${Math.abs(days)} day${Math.abs(days)===1?"":"s"} overdue` };
    if (days === 0) return { cls:"due-soon", days, text:"Due today" };
    if (days <= 7) return { cls:"due-soon", days, text:`In ${days} day${days===1?"":"s"}` };
    return { cls:"", days, text:`In ${days} days` };
  };

  return (
    <div>
      <div className="page-head">
        <div>
          <h1>Fleet</h1>
          <div className="subtitle">Trucks, trailers, and inspection windows. Alerts go to Bill's WhatsApp at 30 / 7 / 0 days.</div>
        </div>
        <button className="btn primary" onClick={() => toast("Add vehicle — coming soon")}><Icon name="plus" size={16} /> Add vehicle</button>
      </div>
      {vList.length === 0 && !loading && (
        <div className="empty-state card" style={{padding: '40px 20px', textAlign: 'center'}}>
          <Icon name="truck" size={32} />
          <div style={{marginTop: 12, fontSize: 15, color: 'var(--ink-2)'}}>No vehicles registered yet.</div>
          <div style={{marginTop: 4, fontSize: 13, color: 'var(--ink-3)'}}>Add a truck or trailer to track inspections, registration, and insurance due dates.</div>
        </div>
      )}
      <div className="fleet-grid">
        {vList.map(v => {
          const insp = dueState(v.inspection_due);
          const reg = dueState(v.registration_due);
          const ins = dueState(v.insurance_due);
          return (
            <div key={v.id} className="vehicle-card">
              <div className="vehicle-head">
                <div className="vehicle-icon"><Icon name="truck" size={22} /></div>
                <div style={{flex:1, minWidth:0}}>
                  <div className="vehicle-label">{v.label}</div>
                  <div className="vehicle-sub">
                    {v.year} {v.make} {v.model} · {v.plate}
                  </div>
                </div>
              </div>
              <div className="vehicle-due-grid">
                <div className={`due-cell ${insp.cls}`}>
                  <div className="dc-label">Inspection</div>
                  <div className="dc-date">{fmtDate(v.inspection_due)}</div>
                  <div className="dc-in">{insp.text}</div>
                </div>
                <div className={`due-cell ${reg.cls}`}>
                  <div className="dc-label">Registration</div>
                  <div className="dc-date">{fmtDate(v.registration_due)}</div>
                  <div className="dc-in">{reg.text}</div>
                </div>
                <div className={`due-cell ${ins.cls}`}>
                  <div className="dc-label">Insurance</div>
                  <div className="dc-date">{fmtDate(v.insurance_due)}</div>
                  <div className="dc-in">{ins.text}</div>
                </div>
                <div className="due-cell">
                  <div className="dc-label">Odometer</div>
                  <div className="dc-date">{v.odometer ? v.odometer.toLocaleString() + ' mi' : '—'}</div>
                  <div className="dc-in">—</div>
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

/* ====================================================================
   Assistant — OpenClaw (REAL: wired to /chat endpoint)
   ==================================================================== */
const SESSION_KEY = 'bsp-hub-session';

// `crypto.randomUUID()` requires a SECURE CONTEXT (HTTPS or localhost) on
// iOS Safari. If the hub is accessed from a phone via plain HTTP at a
// LAN IP / Tailscale name (e.g. `http://chrispc:8787/`), the call is
// undefined and throws. The original IIFE below threw at MODULE LOAD
// TIME — taking the entire views-3.jsx script down with it, which made
// AssistantView, DocumentsView, and FleetView all undefined and rendered
// as a white screen when the user navigated to any of them.
//
// This polyfill returns a v4-shaped UUID using `Math.random` for the
// fallback path. Not cryptographically strong, but a session id doesn't
// need to be — it's just a per-tab identifier the OpenClaw gateway
// uses to pin a conversation. The strong path is still preferred when
// available.
function _makeUUID() {
  try {
    if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
      return crypto.randomUUID();
    }
  } catch {}
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    const r = Math.random() * 16 | 0;
    const v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

let hubSessionId = (() => {
  try {
    let id = localStorage.getItem(SESSION_KEY);
    if (!id) {
      id = _makeUUID();
      try { localStorage.setItem(SESSION_KEY, id); } catch {}
    }
    return id;
  } catch {
    return _makeUUID();
  }
})();

const AssistantView = () => {
  const [history, setHistory] = React.useState([]);
  const [draft, setDraft] = React.useState("");
  const [sending, setSending] = React.useState(false);
  const bodyRef = React.useRef(null);

  React.useEffect(() => {
    if (bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
  }, [history]);

  const send = async (text) => {
    const t = (text ?? draft).trim();
    if (!t || sending) return;
    setHistory(h => [...h, { role: "user", text: t }]);
    setDraft("");
    setSending(true);

    try {
      const resp = await fetch('/chat', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ sessionId: hubSessionId, text: t }),
      });
      const data = await resp.json();
      if (!resp.ok) {
        setHistory(h => [...h, { role: "assistant", text: data.error || "Something went wrong — try again or call (631) 360-1767." }]);
      } else {
        setHistory(h => [...h, { role: "assistant", text: data.reply }]);
      }
    } catch (err) {
      setHistory(h => [...h, { role: "assistant", text: "Couldn't reach the assistant — check that the server and OpenClaw gateway are running." }]);
    } finally {
      setSending(false);
    }
  };

  const suggestions = [
    "Who's late on payment?",
    "Draft a friendly opening reminder for Dale.",
    "How many gallons did I service last week?",
    "What chemicals did I buy this month?",
  ];

  const renderText = (t) => {
    const lines = t.split("\n");
    return lines.map((line, i) => {
      const parts = line.split(/(\*\*[^*]+\*\*)/g).map((p, j) =>
        p.startsWith("**") ? <strong key={j}>{p.slice(2,-2)}</strong> : p
      );
      return <div key={i} style={{minHeight: line ? undefined : "0.6em"}}>{parts}</div>;
    });
  };

  return (
    <div>
      <div className="page-head">
        <div>
          <h1>OpenClaw assistant</h1>
          <div className="subtitle">Same brain as the voice agent. Ask anything about your business — calls, customers, money, schedule.</div>
        </div>
      </div>

      <div className="assistant-frame">
        <div className="assistant-sidebar">
          <div className="assistant-intro">
            <div className="ai-mark">OC</div>
            <h3>OpenClaw</h3>
            <p>I see your calls, customers, appointments, messages, and documents. Pick a starter or just ask.</p>
          </div>
          <div className="assistant-suggestions">
            <h4>Try asking</h4>
            {suggestions.map(s => (
              <button key={s} className="suggest" onClick={() => send(s)}>{s}</button>
            ))}
          </div>
        </div>
        <div className="assistant-main">
          <div className="assistant-body" ref={bodyRef}>
            {/* Empty-state: when there's no chat history, show the intro
              * and starter prompts INSIDE the body. On desktop this
              * duplicates the sidebar (which is also visible), but on
              * mobile the sidebar is `display: none` so without this
              * the body is just empty white space — looks like a
              * broken page. As soon as the user sends one message the
              * empty state disappears and normal chat takes over. */}
            {history.length === 0 && !sending && (
              <div className="assistant-empty">
                <div className="ai-mark">OC</div>
                <h3>OpenClaw</h3>
                <p>Same brain as the voice agent — ask anything about your business.</p>
                <div className="assistant-empty-suggestions">
                  {suggestions.map(s => (
                    <button key={s} className="suggest" onClick={() => send(s)}>{s}</button>
                  ))}
                </div>
              </div>
            )}
            {history.map((m, i) => (
              <div key={i} className={`ai-msg ${m.role === "user" ? "user" : ""}`}>
                <div className="ai-av">{m.role === "user" ? "B" : "OC"}</div>
                <div className="ai-bubble">{renderText(m.text)}</div>
              </div>
            ))}
            {sending && (
              <div className="ai-msg">
                <div className="ai-av">OC</div>
                <div className="ai-bubble" style={{opacity: 0.7, fontStyle: "italic"}}>Thinking…</div>
              </div>
            )}
          </div>
          <div className="compose-row">
            <textarea
              value={draft}
              onChange={e => setDraft(e.target.value)}
              onKeyDown={e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
              placeholder="Ask OpenClaw…"
              rows={1}
              disabled={sending}
            />
            <button className="btn primary" onClick={() => send()} disabled={!draft.trim() || sending}>
              {sending ? "…" : "Send"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

window.DocumentsView = DocumentsView;
window.FleetView = FleetView;
window.AssistantView = AssistantView;
