// UploadDrawer — multi-file upload into the org knowledge base.
//
// Files go straight to POST /ingestion/file (see upload_api.jsx); each upload
// returns an ingestion job that is polled to completion so every row shows
// upload → processing → done/failed. Tables that classify as datasets become
// SQL-queryable by the knowledge agent automatically; documents land in
// retrieval — no extra client work after the job finishes.
//
// The component stays mounted at App level and hides with display:none when
// closed, so in-flight uploads and job polls survive closing the modal
// (same keep-alive reasoning as ChatPage/Connectors in index.html).

const { useState: useUpl, useEffect: useUplEffect, useRef: useUplRef } = React;

const UPL_CONCURRENCY = 2;
const UPL_POLL_MS = 1500;
const UPL_POLL_TIMEOUT_MS = 15 * 60_000; // give up polling a job after 15 min
const UPL_STORED_LIMIT = 200; // ingested-file rows fetched per drawer open (backend caps at 500)

// Extensions that ALWAYS consume one of the org's database slots (the backend
// caps those, see /ingestion/files → limits). CSV/Excel are deliberately not
// pre-checked: the backend classifies some of them as documents, which cost
// nothing, so only it can say no.
const UPL_NATIVE_DB_EXTS = ['.sqlite', '.sqlite3', '.duckdb'];

function uplFmtSize(bytes) {
  if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
  if (bytes >= 1024) return Math.round(bytes / 1024) + ' KB';
  return bytes + ' B';
}

function UploadDrawer({ open, onClose }) {
  const [items, setItems] = useUpl([]);   // {key, file, name, size, status, error, jobId, startedAt}
  const [dragOver, setDragOver] = useUpl(false);
  // What the backend already holds ({files, total, queryable} from GET
  // /ingestion/files) — the queue above only knows about this session.
  const [stored, setStored] = useUpl(null);
  const [storedErr, setStoredErr] = useUpl(false);
  const [confirmId, setConfirmId] = useUpl(null);      // stored row whose × awaits a confirming click
  const [deletingIds, setDeletingIds] = useUpl(new Set());
  const [deleteErrs, setDeleteErrs] = useUpl({});      // id → message for a failed delete
  const inputRef = useUplRef(null);
  const startedRef = useUplRef(new Set()); // keys whose upload task already started
  const pollBusyRef = useUplRef(false);
  const keySeq = useUplRef(0);

  const patch = (key, fields) =>
    setItems(prev => prev.map(it => (it.key === key ? { ...it, ...fields } : it)));

  const addFiles = (fileList) => {
    const next = [];
    // Database-slot precheck: block a native db file locally when the org is
    // already at its cap, instead of uploading 100MB just to hear "no". Counts
    // the slots this queue is about to consume too, so dropping five .sqlite
    // files at once doesn't sail past the check. `stored` unloaded → skip; the
    // backend enforces the cap authoritatively either way.
    const dbLimit = (stored && stored.limits && stored.limits.databases) || 0;
    let dbSlots = dbLimit
      ? (stored.databases || 0) + items.filter(it =>
          ['queued', 'uploading', 'processing'].includes(it.status)
          && UPL_NATIVE_DB_EXTS.includes(parcleUploadExt(it.name))).length
      : 0;
    for (const file of Array.from(fileList || [])) {
      let reason = parcleUploadCheck(file);
      if (!reason && dbLimit && UPL_NATIVE_DB_EXTS.includes(parcleUploadExt(file.name))) {
        if (dbSlots >= dbLimit) {
          reason = `Database limit reached (${dbLimit} max) — delete one first`;
        } else {
          dbSlots++;
        }
      }
      next.push({
        key: 'u' + (++keySeq.current),
        file,
        name: file.name,
        size: file.size,
        status: reason ? 'blocked' : 'queued',
        error: reason,
        jobId: null,
        startedAt: null,
      });
    }
    if (next.length) setItems(prev => [...prev, ...next]);
  };

  // Upload pump: keep up to UPL_CONCURRENCY uploads in flight.
  useUplEffect(() => {
    const uploading = items.filter(it => it.status === 'uploading').length;
    if (uploading >= UPL_CONCURRENCY) return;
    const nextUp = items.find(it => it.status === 'queued' && !startedRef.current.has(it.key));
    if (!nextUp) return;
    startedRef.current.add(nextUp.key);
    patch(nextUp.key, { status: 'uploading' });
    (async () => {
      try {
        const job = await ParcleUploadAPI.ingestFile(nextUp.file, nextUp.name);
        if (job && job.id) {
          patch(nextUp.key, { status: 'processing', jobId: job.id, startedAt: Date.now() });
        } else {
          // No job id — treat the accepted upload as done rather than lying "failed".
          patch(nextUp.key, { status: 'done' });
        }
      } catch (err) {
        patch(nextUp.key, { status: 'failed', error: err.message || 'Upload failed' });
      }
    })();
  }, [items]);

  // Job poll: one ticker for all processing rows.
  useUplEffect(() => {
    if (!items.some(it => it.status === 'processing')) return undefined;
    const t = setInterval(async () => {
      if (pollBusyRef.current) return;
      pollBusyRef.current = true;
      try {
        const processing = items.filter(it => it.status === 'processing');
        for (const it of processing) {
          try {
            const job = await ParcleUploadAPI.job(it.jobId);
            if (job.status === 'done') patch(it.key, { status: 'done' });
            else if (job.status === 'failed') patch(it.key, { status: 'failed', error: job.error || 'Ingestion failed' });
            else if (it.startedAt && Date.now() - it.startedAt > UPL_POLL_TIMEOUT_MS) {
              patch(it.key, { status: 'failed', error: 'Timed out waiting for ingestion' });
            }
          } catch (e) { /* transient poll error — try again next tick */ }
        }
      } finally {
        pollBusyRef.current = false;
      }
    }, UPL_POLL_MS);
    return () => clearInterval(t);
  }, [items]);

  const busy = items.some(it => it.status === 'uploading' || it.status === 'processing');
  const doneCount = items.filter(it => it.status === 'done').length;
  const failedCount = items.filter(it => it.status === 'failed' || it.status === 'blocked').length;

  // Tell the Connectors-page card what the queue is doing. The drawer stays
  // mounted while closed, so uploads keep running with nothing on screen —
  // without this the card would look idle mid-upload.
  useUplEffect(() => {
    parcleAnnounceUpload({
      total: items.length,
      done: doneCount,
      failed: failedCount,
      active: items.filter(it => ['queued', 'uploading', 'processing'].includes(it.status)).length,
      busy,
    });
  }, [items, busy, doneCount, failedCount]);

  const loadStored = async () => {
    try {
      const r = await ParcleUploadAPI.files(UPL_STORED_LIMIT);
      setStored(r);
      setStoredErr(false);
    } catch (e) {
      // Keep whatever was last shown — an open drawer flashing empty on a blip
      // would read as "everything got deleted".
      setStoredErr(true);
    }
  };

  // Fetch on the closed→open edge (the drawer stays mounted while closed, so a
  // mount-time fetch would go stale) and again whenever a batch finishes while
  // open, so just-ingested files move from the queue into the stored list.
  const wasOpen = useUplRef(false);
  useUplEffect(() => {
    if (open && !wasOpen.current) { setConfirmId(null); loadStored(); }
    wasOpen.current = open;
  }, [open]);
  const wasBusy = useUplRef(false);
  useUplEffect(() => {
    if (wasBusy.current && !busy && open) loadStored();
    wasBusy.current = busy;
  }, [busy, open]);

  // An armed "Delete?" disarms by itself — mouseleave alone never fires on
  // touch screens, and a forgotten armed row must not delete on a later
  // absent-minded click.
  useUplEffect(() => {
    if (!confirmId) return undefined;
    const t = setTimeout(() => setConfirmId(null), 4000);
    return () => clearTimeout(t);
  }, [confirmId]);

  const deleteStored = async (f) => {
    setConfirmId(null);
    setDeleteErrs(prev => { const n = { ...prev }; delete n[f.id]; return n; });
    setDeletingIds(prev => new Set(prev).add(f.id));
    try {
      await ParcleUploadAPI.deleteFile(f.id);
      setStored(prev => (prev ? {
        ...prev,
        files: prev.files.filter(x => x.id !== f.id),
        total: Math.max(0, prev.total - 1),
        queryable: Math.max(0, prev.queryable - (f.role === 'database' ? 1 : 0)),
      } : prev));
      // Tell the Connectors card and hero strip to re-read their counts now
      // rather than a poll interval later.
      parcleAnnounceUpload({ refresh: true });
      // The optimistic patch above can't tell which quota pool the file came
      // out of (rows don't carry the datasource kind) — re-read so the
      // databases / CSV-datasets counters free the right slot.
      loadStored();
    } catch (err) {
      setDeleteErrs(prev => ({ ...prev, [f.id]: err.message || 'Delete failed' }));
      // Resync with the backend: if the file was already deleted elsewhere
      // (404) the row leaves honestly; if the delete really failed the row
      // survives the reload and keeps its error.
      loadStored();
    } finally {
      setDeletingIds(prev => { const n = new Set(prev); n.delete(f.id); return n; });
    }
  };

  const clearFinished = () => {
    setItems(prev => prev.filter(it => !['done', 'failed', 'blocked'].includes(it.status)));
  };

  const rowStatus = (it) => {
    switch (it.status) {
      case 'blocked': return { kind: 'error', label: it.error };
      case 'queued': return { kind: 'paused', label: 'Queued' };
      case 'uploading': return { kind: 'syncing', label: 'Uploading…' };
      case 'processing': return { kind: 'syncing', label: 'Processing…' };
      case 'done': return { kind: 'live', label: 'Done' };
      case 'failed': return { kind: 'error', label: it.error || 'Failed' };
      default: return { kind: 'paused', label: it.status };
    }
  };

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,.4)', zIndex: 95,
      display: open ? 'flex' : 'none', alignItems: 'center', justifyContent: 'center',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: 640, maxHeight: '80vh', display: 'flex', flexDirection: 'column',
        background: 'var(--surface-raised)',
        borderRadius: 12, border: '1px solid var(--border)',
        boxShadow: 'var(--shadow-2)', overflow: 'hidden',
        animation: 'slideUp 240ms cubic-bezier(.2,.8,.2,1)',
      }}>
        {/* Header */}
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', gap: 12 }}>
          <VendorLogo vendor="upload" size={28}/>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 14, fontWeight: 500 }}>Upload files</div>
            <div className="t-micro text-tertiary" style={{ marginTop: 2 }}>
              Documents & tables · up to {Math.round(PARCLE_UPLOAD_MAX_BYTES / (1024 * 1024))}MB each
            </div>
          </div>
          <button className="btn sm ghost" onClick={onClose}><Icons.X size={14}/></button>
        </div>

        {/* Body */}
        <div style={{ padding: 20, overflowY: 'auto', flex: 1 }}>
          {/* Drop zone */}
          <div
            onDragOver={e => { e.preventDefault(); setDragOver(true); }}
            onDragLeave={() => setDragOver(false)}
            onDrop={e => { e.preventDefault(); setDragOver(false); addFiles(e.dataTransfer.files); }}
            onClick={() => inputRef.current && inputRef.current.click()}
            style={{
              border: `1px dashed ${dragOver ? 'var(--accent)' : 'var(--border)'}`,
              background: dragOver ? 'var(--hover)' : 'transparent',
              borderRadius: 10, padding: '28px 20px', textAlign: 'center', cursor: 'pointer',
              transition: 'border-color 120ms, background 120ms',
            }}>
            <div className="t-h3">Drop files here or click to browse</div>
            <div className="t-small text-secondary" style={{ marginTop: 4 }}>
              Datasets (CSV / Excel / SQLite / DuckDB) become SQL-queryable; documents become searchable.
            </div>
            {stored && stored.limits && (stored.limits.databases > 0 || stored.limits.csv_datasets > 0) && (
              <div className="t-micro text-tertiary" style={{ marginTop: 4 }}>
                Up to {[
                  stored.limits.databases > 0
                    ? `${stored.limits.databases} database files${stored.limits.tables_per_database > 0 ? ` (${stored.limits.tables_per_database} tables each)` : ''}`
                    : null,
                  stored.limits.csv_datasets > 0 ? `${stored.limits.csv_datasets} CSV datasets` : null,
                ].filter(Boolean).join(' and ')} per workspace.
              </div>
            )}
            <input ref={inputRef} type="file" multiple accept={parcleUploadAccept()}
              style={{ display: 'none' }}
              onChange={e => { addFiles(e.target.files); e.target.value = ''; }}/>
          </div>

          {/* File rows */}
          {items.length > 0 && (
            <div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 6 }}>
              {items.map(it => {
                const st = rowStatus(it);
                return (
                  <div key={it.key} style={{
                    display: 'flex', alignItems: 'center', gap: 10,
                    padding: '8px 12px', border: '1px solid var(--border-subtle)', borderRadius: 8,
                    background: 'var(--surface)',
                  }}>
                    <Icons.Doc size={14} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}/>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div className="t-small" style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{it.name}</div>
                      <div className="mono-sm mono text-tertiary">{uplFmtSize(it.size)}</div>
                    </div>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0, maxWidth: 260 }}>
                      <StatusDot kind={st.kind} size={6}/>
                      <span className="mono-sm mono" style={{
                        color: st.kind === 'error' ? 'var(--error)' : 'var(--text-secondary)',
                        overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                      }} title={st.label}>{st.label}</span>
                    </div>
                  </div>
                );
              })}
            </div>
          )}

          {/* Already-ingested files (server truth) with real deletion */}
          {stored && stored.files.length > 0 && (
            <div style={{ marginTop: 20 }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
                <span className="t-small" style={{ fontWeight: 500 }}>In memory</span>
                <span className="mono-sm mono text-tertiary">
                  {stored.total}
                  {/* Each pool shows only when its cap is actually set — a 0
                      (uncapped) pool must not render as "y/0". */}
                  {stored.limits && (stored.limits.databases > 0 || stored.limits.csv_datasets > 0)
                    ? [
                        stored.limits.databases > 0 ? `${stored.databases || 0}/${stored.limits.databases} databases` : null,
                        stored.limits.csv_datasets > 0 ? `${stored.csv_datasets || 0}/${stored.limits.csv_datasets} CSV datasets` : null,
                      ].filter(Boolean).map(s => ` · ${s}`).join('')
                    : (stored.queryable ? ` · ${stored.queryable} queryable with SQL` : '')}
                </span>
                {storedErr && (
                  <span className="mono-sm mono" style={{ color: 'var(--error)' }}>· refresh failed</span>
                )}
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                {stored.files.map(f => {
                  const deleting = deletingIds.has(f.id);
                  const confirming = confirmId === f.id;
                  const err = deleteErrs[f.id];
                  return (
                    <div key={f.id} style={{
                      display: 'flex', alignItems: 'center', gap: 10,
                      padding: '8px 12px', border: '1px solid var(--border-subtle)', borderRadius: 8,
                      background: 'var(--surface)', opacity: deleting ? 0.6 : 1,
                    }}>
                      <Icons.Doc size={14} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}/>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div className="t-small" style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.name}</div>
                        <div className="mono-sm mono text-tertiary">
                          {new Date(f.ingested_at).toLocaleDateString()}
                          {f.role === 'database' ? ' · SQL' : ''}
                        </div>
                      </div>
                      {err && (
                        <span className="mono-sm mono" style={{ color: 'var(--error)', maxWidth: 180, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={err}>
                          {err}
                        </span>
                      )}
                      {deleting ? (
                        <span className="mono-sm mono text-tertiary" style={{ flexShrink: 0 }}>Deleting…</span>
                      ) : confirming ? (
                        // Deleting really erases the ingested content, so the ×
                        // asks for one confirming click instead of acting at once.
                        <button className="btn sm" style={{ color: 'var(--error)', flexShrink: 0 }}
                          onMouseLeave={() => setConfirmId(null)}
                          onClick={() => deleteStored(f)}>
                          Delete?
                        </button>
                      ) : (
                        <button className="btn sm ghost" style={{ flexShrink: 0 }}
                          title="Delete from memory"
                          onClick={() => setConfirmId(f.id)}>
                          <Icons.X size={13}/>
                        </button>
                      )}
                    </div>
                  );
                })}
                {stored.total > stored.files.length && (
                  <div className="mono-sm mono text-tertiary" style={{ padding: '4px 2px' }}>
                    Showing {stored.files.length} of {stored.total}
                  </div>
                )}
              </div>
            </div>
          )}
          {!stored && storedErr && (
            <div className="mono-sm mono text-tertiary" style={{ marginTop: 16 }}>
              Couldn't load ingested files — they'll appear when Parcle is reachable.
            </div>
          )}
        </div>

        {/* Footer */}
        <div style={{ padding: '14px 20px', borderTop: '1px solid var(--border-subtle)', display: 'flex', gap: 8, alignItems: 'center' }}>
          <span className="mono-sm mono text-tertiary">
            {items.length > 0
              ? `${doneCount} done${failedCount ? ` · ${failedCount} failed` : ''}${busy ? ' · working…' : ''}`
              : 'No files selected yet'}
          </span>
          <div style={{ flex: 1 }}/>
          {items.some(it => ['done', 'failed', 'blocked'].includes(it.status)) && (
            <button className="btn ghost" onClick={clearFinished}>Clear finished</button>
          )}
          <button className="btn" onClick={onClose}>{busy ? 'Continue in background' : 'Close'}</button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { UploadDrawer });
