// =============================================================
// Job Matcher — main App
// =============================================================

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "light",
  "accent": "#7c5cff",
  "density": "comfortable",
  "scoreViz": "dots",
  "showReasoningOnCard": true,
  "scenario": "normal"
}/*EDITMODE-END*/;

const STATUS_DEFS = [
  { key: 'new',          label: 'New',                color: 'var(--status-new)',       collapsedDefault: false },
  { key: 'cover_letter', label: 'Cover letter',       color: 'var(--status-cover)',  collapsedDefault: false },
  { key: 'applied',      label: 'Applied',            color: 'var(--status-applied)',   collapsedDefault: false },
  { key: 'interviewing', label: 'Interviewing',       color: 'var(--status-interview)', collapsedDefault: false },
  { key: 'rejected',     label: 'Rejected',           color: 'var(--status-rejected)',  collapsedDefault: true },
  { key: 'ignored',      label: 'Ignored',            color: 'var(--status-ignored)',   collapsedDefault: true },
];

function App() {
  const [view, setView] = React.useState('board'); // 'board' | 'triage' | 'scatter'
  // Jobs start empty and get hydrated by JobMatcherAPI.loadJobs() in an effect
  // below. This works for both the live (n8n webhook) and the demo (data.js)
  // paths — the API picks the right source based on window.JOB_MATCHER_API.
  const [jobs, setJobs] = React.useState([]);
  // Connection state drives the topbar badge. Possible values mirror api.js:
  // 'demo' | 'connecting' | 'live' | 'error'.
  const [apiStatus, setApiStatus] = React.useState(
    window.JobMatcherAPI ? window.JobMatcherAPI.getStatus() : 'demo'
  );

  // Hydrate jobs once on mount, and subscribe to API status changes so the
  // badge updates if the live connection drops or recovers later.
  React.useEffect(() => {
    let cancelled = false;
    if (!window.JobMatcherAPI) return;
    window.JobMatcherAPI.loadJobs().then((data) => {
      if (cancelled) return;
      // Preserve the original "pre-star a few jobs" demo flourish so the
      // demo dataset still looks lived-in. Real data already carries its
      // own starred flags from the sheet, so this only fires when those
      // slots aren't already starred.
      const next = data.map(j => ({ ...j }));
      [0, 2, 11].forEach(i => { if (next[i] && next[i].starred === undefined) next[i].starred = true; });
      setJobs(next);
    });
    const unsub = window.JobMatcherAPI.onStatusChange(setApiStatus);
    return () => { cancelled = true; unsub && unsub(); };
  }, []);
  const [collapsed, setCollapsed] = React.useState(() => {
    const c = {};
    for (const s of STATUS_DEFS) c[s.key] = s.collapsedDefault;
    return c;
  });
  const [detailId, setDetailId] = React.useState(null);
  const [bulkSet, setBulkSet] = React.useState(() => new Set());
  const [undoState, setUndoState] = React.useState(null);
  const [search, setSearch] = React.useState('');
  const [scoreRange, setScoreRange] = React.useState([0, 100]);
  const [matchRange, setMatchRange] = React.useState([0, 100]);
  const [salaryMin, setSalaryMin] = React.useState(0);
  const [remoteFilter, setRemoteFilter] = React.useState('any'); // any | remote | onsite
  const [seniorityFilter, setSeniorityFilter] = React.useState([]); // strings
  const [employmentFilter, setEmploymentFilter] = React.useState([]);
  const [starredOnly, setStarredOnly] = React.useState(false);
  const [settingsOpen, setSettingsOpen] = React.useState(false);
  const [workflow, setWorkflow] = React.useState({
    status: 'idle', lastRun: '07:02 today', scraped: 24, scored: 22, errors: 0, progress: ''
  });

  const [t, setTweak] = window.useTweaks
    ? window.useTweaks(TWEAK_DEFAULTS)
    : [TWEAK_DEFAULTS, () => {}];

  // Apply theme to document
  React.useEffect(() => {
    document.documentElement.dataset.theme = t.theme;
    document.documentElement.dataset.density = t.density;
    document.documentElement.style.setProperty('--accent', t.accent);
    document.documentElement.style.setProperty('--accent-soft', toAccentSoft(t.accent));
  }, [t.theme, t.density, t.accent]);

  // Bulk mode auto-toggles when there are selections
  const bulkMode = bulkSet.size > 0;

  // ---------- Filtering ----------
  const seniorityOptions = React.useMemo(() => uniq(jobs.map(j => j.seniority).filter(s => s && s !== 'Not Applicable')), [jobs]);
  const employmentOptions = React.useMemo(() => uniq(jobs.map(j => j.employment).filter(Boolean)), [jobs]);

  const filtered = jobs.filter(j => {
    if (search) {
      const q = search.toLowerCase();
      if (!(j.title?.toLowerCase().includes(q) ||
            j.company?.toLowerCase().includes(q) ||
            j.description?.toLowerCase().includes(q))) return false;
    }
    if (j.cvFitness < scoreRange[0] || j.cvFitness > scoreRange[1]) return false;
    if (j.matchScore < matchRange[0] || j.matchScore > matchRange[1]) return false;
    if (salaryMin > 0) {
      const s = parseSalaryLow(j.salary);
      if (s === null || s < salaryMin) return false;
    }
    if (remoteFilter === 'remote' && !j.isRemote) return false;
    if (remoteFilter === 'onsite' && j.isRemote) return false;
    if (seniorityFilter.length && !seniorityFilter.includes(j.seniority)) return false;
    if (employmentFilter.length && !employmentFilter.includes(j.employment)) return false;
    if (starredOnly && !j.starred) return false;
    return true;
  });

  // Group by status
  const byStatus = {};
  for (const s of STATUS_DEFS) byStatus[s.key] = [];
  for (const j of filtered) {
    if (byStatus[j.status]) byStatus[j.status].push(j);
  }
  // Sort each column
  for (const s of STATUS_DEFS) {
    byStatus[s.key].sort((a, b) => (b.matchScore + b.cvFitness/2) - (a.matchScore + a.cvFitness/2));
  }

  // ---------- Mutations ----------
  // Persist anything that should round-trip to the sheet. Internal-only
  // flags (those prefixed with `_`, like _coverFlow) stay local — they
  // describe transient UI state, not durable job data.
  const PERSIST_FIELDS = new Set([
    'status', 'starred', 'coverLetter', 'coverLetterNotes',
  ]);
  const filterPersistable = (patch) => {
    const out = {};
    for (const [k, v] of Object.entries(patch)) {
      if (PERSIST_FIELDS.has(k)) out[k] = v;
    }
    return out;
  };
  const updateJob = (id, patch) => {
    setJobs(js => js.map(j => j.id === id ? { ...j, ...patch } : j));
    const persistable = filterPersistable(patch);
    if (window.JobMatcherAPI && Object.keys(persistable).length) {
      window.JobMatcherAPI.patchJob(id, persistable);
    }
  };
  const updateMany = (ids, patch) => {
    setJobs(js => js.map(j => ids.includes(j.id) ? { ...j, ...patch } : j));
    const persistable = filterPersistable(patch);
    if (window.JobMatcherAPI && Object.keys(persistable).length) {
      ids.forEach(id => window.JobMatcherAPI.patchJob(id, persistable));
    }
  };

  const ignoreJob = (id) => {
    const job = jobs.find(j => j.id === id);
    if (!job) return;
    const prev = job.status;
    updateJob(id, { status: 'ignored', _coverFlow: null });
    triggerUndo(`Moved "${job.title}" to Ignored`, () => updateJob(id, { status: prev }));
  };
  const unignoreJob = (id) => {
    const job = jobs.find(j => j.id === id);
    if (!job) return;
    updateJob(id, { status: 'new' });
  };
  const markApplied = (id) => {
    const job = jobs.find(j => j.id === id);
    if (!job) return;
    const prev = job.status;
    updateJob(id, { status: 'applied' });
    triggerUndo(`Marked "${job.title}" as applied`, () => updateJob(id, { status: prev }));
  };
  const startCoverLetter = (id) => {
    updateJob(id, { _coverFlow: 'form', coverLetterNotes: '' });
  };
  const submitCoverLetterForm = (id) => {
    updateJob(id, { _coverFlow: 'loading' });
  };
  const coverLetterReady = (id) => {
    const job = jobs.find(j => j.id === id);
    if (!job) return;
    // Generate a draft using the existing template
    const draft = job.coverLetter || generateDraftFallback(job);
    updateJob(id, { _coverFlow: 'preview', coverLetter: draft });
  };
  const acceptCoverLetter = (id) => {
    updateJob(id, { _coverFlow: null, status: 'cover_letter' });
  };
  const cancelCoverLetter = (id) => {
    updateJob(id, { _coverFlow: null });
  };
  const updateCoverLetterText = (id, text) => updateJob(id, { coverLetter: text });
  const updateCoverLetterNotes = (id, text) => updateJob(id, { coverLetterNotes: text });
  const toggleStar = (id) => {
    const job = jobs.find(j => j.id === id);
    if (!job) return;
    updateJob(id, { starred: !job.starred });
  };

  // ---------- Undo ----------
  const triggerUndo = (msg, undo) => {
    setUndoState({ msg, undo });
    setTimeout(() => setUndoState(s => (s && s.msg === msg ? null : s)), 5000);
  };

  // ---------- Bulk ----------
  const toggleBulk = (id) => {
    setBulkSet(s => {
      const next = new Set(s);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };
  const clearBulk = () => setBulkSet(new Set());
  const bulkIgnore = () => {
    const ids = [...bulkSet];
    const snapshot = jobs.filter(j => ids.includes(j.id)).map(j => ({ id: j.id, status: j.status }));
    updateMany(ids, { status: 'ignored' });
    clearBulk();
    triggerUndo(`Moved ${ids.length} jobs to Ignored`, () =>
      setJobs(js => js.map(j => {
        const s = snapshot.find(x => x.id === j.id);
        return s ? { ...j, status: s.status } : j;
      }))
    );
  };

  // ---------- Refresh workflow ----------
  // In demo mode (no API configured) we keep the old fake-progress animation
  // so the page still feels alive. In live mode we hit n8n's POST /run, which
  // returns the freshly recommended jobs in the response body — no polling.
  const runWorkflow = async () => {
    const isLive = !!window.JobMatcherAPI && apiStatus !== 'demo';

    if (!isLive) {
      setWorkflow(w => ({ ...w, status: 'running', progress: 'fetching emails…', errors: 0, lastError: null }));
      const steps = [
        ['fetching emails…', 700],
        ['extracting job links…', 600],
        ['scraping LinkedIn…', 1200],
        ['scoring CV fitness…', 900],
        ['scoring interest match…', 800],
      ];
      let cum = 0;
      steps.forEach(([msg, dur], i) => {
        cum += dur;
        setTimeout(() => {
          if (i === steps.length - 1) {
            setTimeout(() => {
              setWorkflow({ status: 'idle', lastRun: 'just now', scraped: 27, scored: 26, errors: 0, progress: '' });
            }, dur);
          } else {
            setWorkflow(w => ({ ...w, progress: msg }));
          }
        }, cum);
      });
      return;
    }

    setWorkflow(w => ({ ...w, status: 'running', progress: 'running n8n pipeline…', errors: 0, lastError: null }));
    try {
      const newJobs = await window.JobMatcherAPI.runWorkflow();
      // Merge by id: update existing rows, append new ones, preserve local
      // overrides that were already applied during loadJobs().
      setJobs(prev => {
        const byId = new Map(prev.map(j => [j.id, j]));
        for (const nj of newJobs) byId.set(nj.id, { ...(byId.get(nj.id) || {}), ...nj });
        return Array.from(byId.values());
      });
      setWorkflow({
        status: 'idle',
        lastRun: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
        scraped: newJobs.length,
        scored: newJobs.length,
        errors: 0,
        progress: '',
        lastError: null,
      });
    } catch (err) {
      console.error('[runWorkflow] failed:', err);
      setWorkflow(w => ({
        ...w,
        status: 'error',
        progress: '',
        errors: (w.errors || 0) + 1,
        lastError: err.message || String(err),
      }));
    }
  };

  // ---------- Loading-step coverletter chain ----------
  // job-card emits onSubmit after loading completes → we transition to preview
  const handleCLSubmit = (id) => {
    // Called when form submit (begin loading) AND when loading completes (go to preview)
    const j = jobs.find(x => x.id === id);
    if (!j) return;
    if (j._coverFlow === 'form') {
      submitCoverLetterForm(id);
    } else if (j._coverFlow === 'loading') {
      coverLetterReady(id);
    }
  };

  const detailJob = jobs.find(j => j.id === detailId) || null;

  const totalFiltered = filtered.length;
  const totalJobs = jobs.length;

  return (
    <>
      {/* Top bar */}
      <header className="topbar">
        <div className="brand">
          <div className="brand-mark mono">JM</div>
          <div>
            <span className="brand-name">Job Matcher</span>
            <span className="brand-sub mono"> · v2</span>
          </div>
        </div>
        <div className="topbar-spacer"></div>
        <ConnectionBadge status={apiStatus} />
        <WorkflowStatus workflow={workflow} onRefresh={runWorkflow} />
        <button className="icon-btn primary" onClick={runWorkflow} title="Run workflow now"
          disabled={workflow.status === 'running'}>
          <Icon.refresh style={workflow.status === 'running' ? { animation: 'spin 1s linear infinite' } : {}} />
        </button>
        <button className="icon-btn" onClick={() => setSettingsOpen(true)} title="Settings">
          <Icon.settings />
        </button>
      </header>

      {/* Filters bar */}
      <div className="filters-bar">
        <div className="view-switcher">
          <button className={view === 'board' ? 'active' : ''} onClick={() => { setView('board'); setDetailId(null); }}>
            <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
              <rect x="2" y="3" width="3" height="10" rx="0.5"/>
              <rect x="6.5" y="3" width="3" height="6" rx="0.5"/>
              <rect x="11" y="3" width="3" height="8" rx="0.5"/>
            </svg>
            Board
          </button>
          <button className={view === 'triage' ? 'active' : ''} onClick={() => { setView('triage'); setDetailId(null); }}>
            <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
              <rect x="2" y="3" width="4" height="10" rx="0.5"/>
              <rect x="7" y="3" width="7" height="10" rx="0.5"/>
            </svg>
            Triage
          </button>
          <button className={view === 'scatter' ? 'active' : ''} onClick={() => { setView('scatter'); setDetailId(null); }}>
            <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
              <path d="M2 14h12M2 2v12" />
              <circle cx="5" cy="10" r="1" fill="currentColor"/>
              <circle cx="8" cy="7" r="1" fill="currentColor"/>
              <circle cx="11" cy="4" r="1" fill="currentColor"/>
            </svg>
            Scatter
          </button>
        </div>

        <div className="search-wrap">
          <Icon.search />
          <input
            placeholder="Search titles, companies, keywords…"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
          />
        </div>

        <button className={`filter-chip${starredOnly ? ' active' : ''}`}
          onClick={() => setStarredOnly(v => !v)}
          title={starredOnly ? 'Showing starred only' : 'Show starred only'}>
          {starredOnly ? <Icon.starFilled style={{ color: 'oklch(0.7 0.18 80)' }} /> : <Icon.star />}
          <span>Starred</span>
          {starredOnly && (
            <span className="value mono">{jobs.filter(j => j.starred).length}</span>
          )}
        </button>

        <FilterChip
          label="CV fit"
          active={scoreRange[0] > 0 || scoreRange[1] < 100}
          value={`${scoreRange[0]}–${scoreRange[1]}`}
          onClear={() => setScoreRange([0, 100])}>
          {() => (
            <>
              <h4>CV fitness score</h4>
              <DualRange min={0} max={100} value={scoreRange} step={5} onChange={setScoreRange} />
              <div className="mono" style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--text-faint)' }}>
                <span>{scoreRange[0]}</span><span>{scoreRange[1]}</span>
              </div>
            </>
          )}
        </FilterChip>

        <FilterChip
          label="Match"
          active={matchRange[0] > 0 || matchRange[1] < 100}
          value={`${matchRange[0]}–${matchRange[1]}`}
          onClear={() => setMatchRange([0, 100])}>
          {() => (
            <>
              <h4>Interest match score</h4>
              <DualRange min={0} max={100} value={matchRange} step={5} onChange={setMatchRange} />
              <div className="mono" style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--text-faint)' }}>
                <span>{matchRange[0]}</span><span>{matchRange[1]}</span>
              </div>
            </>
          )}
        </FilterChip>

        <FilterChip
          label="Salary"
          active={salaryMin > 0}
          value={salaryMin > 0 ? `≥ €${salaryMin}k` : null}
          onClear={() => setSalaryMin(0)}>
          {() => (
            <>
              <h4>Minimum salary</h4>
              <input type="range" min={0} max={200} step={5} value={salaryMin}
                onChange={(e) => setSalaryMin(+e.target.value)} />
              <div className="mono" style={{ fontSize: 12, color: 'var(--text)', textAlign: 'center' }}>
                {salaryMin === 0 ? 'any' : `€${salaryMin}k+`}
              </div>
              <div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 6 }}>
                Jobs without stated salary are kept (set min to 0).
              </div>
            </>
          )}
        </FilterChip>

        <FilterChip
          label="Remote"
          active={remoteFilter !== 'any'}
          value={remoteFilter !== 'any' ? remoteFilter : null}
          onClear={() => setRemoteFilter('any')}>
          {(close) => (
            <>
              <h4>Work location</h4>
              {['any', 'remote', 'onsite'].map(o => (
                <div key={o} className="pop-row" onClick={() => { setRemoteFilter(o); close(); }}>
                  <input type="radio" readOnly checked={remoteFilter === o} />
                  <span style={{ textTransform: 'capitalize' }}>{o}</span>
                </div>
              ))}
            </>
          )}
        </FilterChip>

        <FilterChip
          label="Seniority"
          active={seniorityFilter.length > 0}
          value={seniorityFilter.length ? `${seniorityFilter.length} selected` : null}
          onClear={() => setSeniorityFilter([])}>
          {() => (
            <>
              <h4>Seniority</h4>
              {seniorityOptions.map(o => (
                <label key={o} className="pop-row">
                  <input type="checkbox"
                    checked={seniorityFilter.includes(o)}
                    onChange={(e) => setSeniorityFilter(s =>
                      e.target.checked ? [...s, o] : s.filter(x => x !== o)
                    )} />
                  <span>{o}</span>
                </label>
              ))}
            </>
          )}
        </FilterChip>

        <FilterChip
          label="Type"
          active={employmentFilter.length > 0}
          value={employmentFilter.length ? `${employmentFilter.length} selected` : null}
          onClear={() => setEmploymentFilter([])}>
          {() => (
            <>
              <h4>Employment type</h4>
              {employmentOptions.map(o => (
                <label key={o} className="pop-row">
                  <input type="checkbox"
                    checked={employmentFilter.includes(o)}
                    onChange={(e) => setEmploymentFilter(s =>
                      e.target.checked ? [...s, o] : s.filter(x => x !== o)
                    )} />
                  <span>{o}</span>
                </label>
              ))}
            </>
          )}
        </FilterChip>

        <div style={{ flex: 1 }}></div>
        <div className="mono" style={{ fontSize: 11, color: 'var(--text-faint)', whiteSpace: 'nowrap' }}>
          {totalFiltered} / {totalJobs} jobs
        </div>
      </div>

      {/* Board / Triage / Scatter */}
      {view === 'board' && (
        <div className={`board${bulkMode ? ' bulk-mode' : ''}`}>
          {STATUS_DEFS.map(s => (
            <KanbanColumn
              key={s.key}
              status={s.key}
              statusLabel={s.label}
              statusColor={s.color}
              jobs={byStatus[s.key]}
              collapsed={collapsed[s.key]}
              onToggleCollapse={() => setCollapsed(c => ({ ...c, [s.key]: !c[s.key] }))}
              onDropJob={(jobId, newStatus) => {
                const job = jobs.find(j => j.id === jobId);
                // No-op if the card was dropped on its current column.
                if (!job || job.status === newStatus) return;
                const prev = job.status;
                // Also clear any in-flight cover-letter UI state when the
                // card moves columns — otherwise an interrupted CL form
                // would resurface in a column where it doesn't belong.
                updateJob(jobId, { status: newStatus, _coverFlow: null });
                triggerUndo(
                  `Moved "${job.title}" to ${s.label}`,
                  () => updateJob(jobId, { status: prev })
                );
              }}>
              {byStatus[s.key].map(job => (
                <JobCard
                  key={job.id}
                  job={job}
                  selected={detailId === job.id}
                  bulkMode={bulkMode}
                  bulkChecked={bulkSet.has(job.id)}
                  onSelect={() => toggleBulk(job.id)}
                  onOpenDetail={() => setDetailId(job.id)}
                  onIgnore={() => ignoreJob(job.id)}
                  onUnignore={() => unignoreJob(job.id)}
                  onMarkApplied={() => markApplied(job.id)}
                  onCoverLetter={() => startCoverLetter(job.id)}
                  onSubmitCoverLetter={() => handleCLSubmit(job.id)}
                  onAcceptCoverLetter={() => acceptCoverLetter(job.id)}
                  onCancelCoverLetter={() => cancelCoverLetter(job.id)}
                  onUpdateCoverLetter={(text) => updateCoverLetterText(job.id, text)}
                  onUpdateNotes={(text) => updateCoverLetterNotes(job.id, text)}
                  onToggleStar={() => toggleStar(job.id)}
                  scoreViz={t.scoreViz}
                  density={t.density}
                />
              ))}
            </KanbanColumn>
          ))}
        </div>
      )}

      {view === 'triage' && (
        <TriageView
          jobs={filtered}
          selectedId={detailId}
          onSelect={setDetailId}
          onIgnore={ignoreJob}
          onUnignore={unignoreJob}
          onMarkApplied={markApplied}
          onCoverLetter={startCoverLetter}
          onStatusChange={(id, s) => updateJob(id, { status: s })}
          onSubmitCoverLetter={handleCLSubmit}
          onAcceptCoverLetter={acceptCoverLetter}
          onCancelCoverLetter={cancelCoverLetter}
          onUpdateCoverLetter={updateCoverLetterText}
          onUpdateNotes={updateCoverLetterNotes}
          onToggleStar={toggleStar}
          scoreViz={t.scoreViz}
        />
      )}

      {view === 'scatter' && (
        <ScatterView
          jobs={filtered}
          onSelect={setDetailId}
          scoreViz={t.scoreViz}
        />
      )}

      {/* Bulk action bar */}
      {bulkMode && (
        <div className="bulk-bar">
          <span className="count">{bulkSet.size} selected</span>
          <button className="bulk-btn" onClick={clearBulk}>Cancel</button>
          <button className="bulk-btn danger" onClick={bulkIgnore}>
            <Icon.archive style={{ verticalAlign: 'middle', marginRight: 4 }} /> Ignore all
          </button>
        </div>
      )}

      {/* Undo toast */}
      {undoState && !bulkMode && (
        <div className="toast">
          <span>{undoState.msg}</span>
          <button onClick={() => { undoState.undo(); setUndoState(null); }}>Undo</button>
        </div>
      )}

      {/* Detail panel (used in Board & Scatter views; Triage has its own inline detail) */}
      {detailJob && view !== 'triage' && (
        <DetailPanel
          job={detailJob}
          onClose={() => setDetailId(null)}
          onIgnore={() => { ignoreJob(detailJob.id); setDetailId(null); }}
          onUnignore={() => unignoreJob(detailJob.id)}
          onMarkApplied={() => markApplied(detailJob.id)}
          onCoverLetter={() => { startCoverLetter(detailJob.id); setDetailId(null); }}
          onStatusChange={(s) => updateJob(detailJob.id, { status: s })}
          onToggleStar={() => toggleStar(detailJob.id)}
        />
      )}

      {/* Settings */}
      <SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)}
        settings={{}} setSettings={() => {}} />

      {/* Tweaks panel */}
      <TweaksHost t={t} setTweak={setTweak} />
    </>
  );
}

// =============================================================
// Tweaks panel
// =============================================================
function TweaksHost({ t, setTweak }) {
  if (!window.TweaksPanel) return null;
  const { TweaksPanel, TweakSection, TweakRadio, TweakSelect, TweakToggle, TweakColor } = window;
  return (
    <TweaksPanel>
      <TweakSection label="Theme">
        <TweakRadio label="Mode" value={t.theme}
          options={[{ value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }, { value: 'paper', label: 'Paper' }]}
          onChange={(v) => setTweak('theme', v)} />
        <TweakColor label="Accent" value={t.accent}
          options={['#7c5cff', '#d9744a', '#1f8a5b', '#2a6fdb', '#b03f6f']}
          onChange={(v) => setTweak('accent', v)} />
      </TweakSection>

      <TweakSection label="Layout">
        <TweakRadio label="Density" value={t.density}
          options={[{ value: 'comfortable', label: 'Comfortable' }, { value: 'compact', label: 'Compact' }]}
          onChange={(v) => setTweak('density', v)} />
        <TweakRadio label="Score visual" value={t.scoreViz}
          options={[{ value: 'dots', label: 'Dot grid' }, { value: 'quadrant', label: 'Quadrant' }]}
          onChange={(v) => setTweak('scoreViz', v)} />
      </TweakSection>

      <TweakSection label="Data">
        <TweakSelect label="Scenario" value={t.scenario}
          options={[
            { value: 'normal', label: 'Normal pipeline' },
            { value: 'empty', label: 'Empty (nothing today)' },
            { value: 'error', label: 'Workflow errored' }
          ]}
          onChange={(v) => {
            setTweak('scenario', v);
            // Effect handled by app via tweak listener (simple version: page reload)
            window.dispatchEvent(new CustomEvent('scenario-change', { detail: v }));
          }} />
      </TweakSection>
    </TweaksPanel>
  );
}

// =============================================================
// Connection badge — tiny topbar pill showing live/demo/error
// =============================================================
// A pure presentational component. The colour mapping uses inline styles so
// it stays self-contained and doesn't depend on extra CSS classes — easy to
// remove later if you'd rather style it in styles.css.
function ConnectionBadge({ status }) {
  const map = {
    live:       { label: 'Live · n8n', dot: 'oklch(0.72 0.16 145)', tip: 'Connected to your n8n webhook' },
    connecting: { label: 'Connecting…', dot: 'oklch(0.78 0.12 85)',  tip: 'Reaching the n8n webhook' },
    error:      { label: 'Offline',     dot: 'oklch(0.65 0.18 25)',  tip: 'Webhook unreachable — using local cache; writes are queued' },
    demo:       { label: 'Demo data',   dot: 'var(--text-faint)',    tip: 'No webhook configured — using data.js' },
  };
  const m = map[status] || map.demo;
  return (
    <div
      className="conn-badge mono"
      title={m.tip}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: '4px 8px', borderRadius: 999,
        background: 'var(--surface-2, rgba(0,0,0,0.04))',
        border: '1px solid var(--border, rgba(0,0,0,0.08))',
        fontSize: 11, color: 'var(--text-faint)', whiteSpace: 'nowrap',
      }}>
      <span style={{
        width: 6, height: 6, borderRadius: '50%',
        background: m.dot,
        boxShadow: status === 'connecting' ? `0 0 0 0 ${m.dot}` : 'none',
        animation: status === 'connecting' ? 'pulse 1.4s ease-out infinite' : 'none',
      }}/>
      <span>{m.label}</span>
    </div>
  );
}

// =============================================================
// Helpers
// =============================================================
function uniq(arr) { return Array.from(new Set(arr)); }
function parseSalaryLow(s) {
  if (!s) return null;
  const m = s.match(/€\s*(\d+)\s*k/i);
  return m ? parseInt(m[1]) : null;
}
function toAccentSoft(hex) {
  // crude alpha overlay → output rgba
  if (!hex || !hex.startsWith('#')) return 'rgba(124, 92, 255, 0.12)';
  const v = hex.slice(1);
  const n = v.length === 3 ? v.split('').map(c => c + c).join('') : v;
  const r = parseInt(n.slice(0, 2), 16);
  const g = parseInt(n.slice(2, 4), 16);
  const b = parseInt(n.slice(4, 6), 16);
  return `rgba(${r}, ${g}, ${b}, 0.13)`;
}
function generateDraftFallback(job) {
  return `Dear Hiring Team at ${job.company},

I'm writing to express my enthusiasm for the ${job.title} role.

[draft generated from your CV, preferences, and the job description]

Best,
Stefano`;
}

// Listen for scenario changes
window.addEventListener('scenario-change', (e) => {
  // Could mutate state externally; for now this is a placeholder hook.
});

// Mount
const root = ReactDOM.createRoot(document.getElementById('root'));
function tryMount() {
  if (window.JOBS_DATA) root.render(<App />);
  else setTimeout(tryMount, 30);
}
tryMount();
