import React, { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import type { Session } from "@supabase/supabase-js";
import { CheckCircle2, ExternalLink, LogOut, RefreshCw, Send } from "lucide-react";
import { supabase } from "./supabase";
import { Badge, Collapsible, Hero, Stat, TableCard } from "./components";
import { FIT_LABELS, LOCAL_RUNNER_URL, TIER_ORDER, VETTING_STATUSES } from "./constants";
import type {
  BrandDetailRpcRow,
  BrandFilters,
  BrandCrmExport,
  BrandSummaryRow,
  CreatorNiche,
  Filters,
  LinkRow,
  OpsSnapshot,
  PipelineRun,
  QuotaLog,
  RunnerStatus,
  SearchKeyword,
  SortKey,
  TierSnapshotRow,
  Video,
} from "./types";
import {
  badgeTone,
  brandFiltersToSearch,
  classifyLink,
  computeCreatorMetrics,
  extractEmailFromText,
  extractSocialLinksFromText,
  filtersToSearch,
  fmtDate,
  fmtFloat,
  fmtInt,
  fmtPct,
  fmtRelative,
  label,
  parseRoute,
  parseSocialLinks,
  truncate,
} from "./utils";
import "./styles.css";

type DerivedMetrics = ReturnType<typeof computeCreatorMetrics>;
const ADD_NICHE_VALUE = "__add_niche__";

async function fetchAllCreatorNicheSnapshot(niche = "") {
  const pageSize = 1000;
  const rows: CreatorNiche[] = [];
  for (let from = 0; ; from += pageSize) {
    const to = from + pageSize - 1;
    let query = supabase
      .from("creator_niches")
      .select("niche_config,channel_id,vetting_status,creators(tier)")
      .order("niche_config", { ascending: true })
      .order("channel_id", { ascending: true })
      .range(from, to);
    if (niche) query = query.eq("niche_config", niche);
    const result = await query;
    if (result.error) throw result.error;
    const page = (result.data ?? []) as CreatorNiche[];
    rows.push(...page);
    if (page.length < pageSize) return rows;
  }
}

async function fetchAllNicheNames() {
  const result = await supabase.rpc("niche_options");
  if (result.error) throw result.error;
  return ((result.data ?? []) as Array<{ niche_config: string }>).map((row) => row.niche_config);
}

function boardSearch(filters: Filters) {
  return filtersToSearch(filters);
}

function brandsSearch(filters: BrandFilters) {
  return brandFiltersToSearch(filters);
}

function defaultNiche(niches: string[], runnerConfigs: RunnerStatus["configs"] = []) {
  return Array.from(new Set([...niches, ...runnerConfigs.map((config) => config.name)])).sort()[0] ?? "";
}

async function fetchTierSnapshot(niche = "") {
  const result = await supabase.rpc("dashboard_tier_snapshot", { p_niche_config: niche || null });
  if (result.error) throw result.error;
  return (result.data ?? []) as TierSnapshotRow[];
}

async function fetchOpsSnapshot(niche: string) {
  if (!niche) return { total: 0, discovered: 0, stage1: 0, vetted: 0, disqualified: 0 };
  const result = await supabase.rpc("ops_niche_snapshot", { p_niche_config: niche });
  if (result.error) throw result.error;
  return ((result.data ?? [])[0] ?? { total: 0, discovered: 0, stage1: 0, vetted: 0, disqualified: 0 }) as OpsSnapshot;
}

async function fetchBrandSummary(filters: BrandFilters) {
  const result = await supabase.rpc("brand_summary", {
    p_niche_config: filters.niche || null,
    p_mention_type: filters.mentionType === "all" ? null : filters.mentionType,
    p_domain_query: filters.q.trim() || null,
  });
  if (result.error) throw result.error;
  return (result.data ?? []) as BrandSummaryRow[];
}

async function fetchBrandExports(niche = "") {
  let query = supabase
    .from("brand_crm_exports")
    .select("domain,niche_config,airtable_brand_record_id,synced_at,sync_error");
  query = niche ? query.eq("niche_config", niche) : query.eq("niche_config", "");
  const result = await query;
  if (result.error) throw result.error;
  return (result.data ?? []) as BrandCrmExport[];
}

function mergeBrandExports(brands: BrandSummaryRow[], exports: BrandCrmExport[]) {
  const byDomain = new Map(exports.map((row) => [row.domain, row]));
  return brands.map((brand) => {
    const crm = byDomain.get(brand.domain);
    return crm ? {
      ...brand,
      airtable_brand_record_id: crm.airtable_brand_record_id,
      airtable_brand_synced_at: crm.synced_at,
      airtable_brand_sync_error: crm.sync_error,
    } : brand;
  });
}

async function fetchBrandDetail(domain: string, filters: BrandFilters) {
  const result = await supabase.rpc("brand_detail_links", {
    p_domain: domain,
    p_niche_config: filters.niche || null,
    p_mention_type: filters.mentionType === "all" ? null : filters.mentionType,
  });
  if (result.error) throw result.error;
  return (result.data ?? []) as BrandDetailRpcRow[];
}

async function fetchQuotaLogsForDate(runDate: string) {
  const pageSize = 1000;
  const rows: QuotaLog[] = [];
  for (let from = 0; ; from += pageSize) {
    const to = from + pageSize - 1;
    const result = await supabase
      .from("api_quota_log")
      .select("*")
      .eq("run_date", runDate)
      .order("created_at", { ascending: false })
      .range(from, to);
    if (result.error) throw result.error;
    const page = (result.data ?? []) as QuotaLog[];
    rows.push(...page);
    if (page.length < pageSize) return rows;
  }
}

function AuthGate({ onSession }: { onSession: (session: Session) => void }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [mode, setMode] = useState<"signin" | "signup">("signin");
  const [message, setMessage] = useState("");
  const [loading, setLoading] = useState(false);

  async function submit(event: FormEvent) {
    event.preventDefault();
    setLoading(true);
    setMessage("");
    const result = mode === "signin"
      ? await supabase.auth.signInWithPassword({ email, password })
      : await supabase.auth.signUp({ email, password });
    setLoading(false);
    if (result.error) setMessage(result.error.message);
    else if (result.data.session) onSession(result.data.session);
    else setMessage("Check your email to confirm this account.");
  }

  return (
    <main className="auth-shell">
      <form onSubmit={submit} className="card auth-card">
        <div className="card-head">
          <h1>Creator review board</h1>
          <p>Sign in with your VA credentials.</p>
        </div>
        <div><div className="label">Email</div><input className="field" type="email" value={email} onChange={(event) => setEmail(event.target.value)} required /></div>
        <div><div className="label">Password</div><input className="field" type="password" value={password} onChange={(event) => setPassword(event.target.value)} required /></div>
        {message ? <div className="notice error">{message}</div> : null}
        <div className="stack">
          <button className="btn" disabled={loading}>{mode === "signin" ? "Sign in" : "Create account"}</button>
          <button type="button" className="btn secondary" onClick={() => setMode(mode === "signin" ? "signup" : "signin")}>
            {mode === "signin" ? "Need account" : "Have account"}
          </button>
        </div>
      </form>
    </main>
  );
}

function App() {
  const [session, setSession] = useState<Session | null>(null);
  const [route, setRoute] = useState<RouteState>(() => parseRoute());
  const [rows, setRows] = useState<CreatorNiche[]>([]);
  const [tierSnapshot, setTierSnapshot] = useState<TierSnapshotRow[]>([]);
  const [opsSnapshot, setOpsSnapshot] = useState<OpsSnapshot>({ total: 0, discovered: 0, stage1: 0, vetted: 0, disqualified: 0 });
  const [niches, setNiches] = useState<string[]>([]);
  const [filters, setFiltersState] = useState<Filters>(() => parseRoute().filters);
  const [brandFilters, setBrandFiltersState] = useState<BrandFilters>(() => parseRoute().brandFilters);
  const [detailRow, setDetailRow] = useState<CreatorNiche | null>(null);
  const [videos, setVideos] = useState<Video[]>([]);
  const [links, setLinks] = useState<LinkRow[]>([]);
  const [brandLinks, setBrandLinks] = useState<BrandSummaryRow[]>([]);
  const [boardMetrics, setBoardMetrics] = useState<Record<string, DerivedMetrics>>({});
  const [brandDetailLinks, setBrandDetailLinks] = useState<LinkRow[]>([]);
  const [brandDetailCrm, setBrandDetailCrm] = useState<BrandCrmExport | null>(null);
  const [pipelineRuns, setPipelineRuns] = useState<PipelineRun[]>([]);
  const [quotaLogs, setQuotaLogs] = useState<QuotaLog[]>([]);
  const [searchKeywords, setSearchKeywords] = useState<SearchKeyword[]>([]);
  const [runnerStatus, setRunnerStatus] = useState<RunnerStatus | null>(null);
  const [runnerBusy, setRunnerBusy] = useState(false);
  const [saving, setSaving] = useState(false);
  const [airtableBusy, setAirtableBusy] = useState("");
  const [loading, setLoading] = useState(true);
  const [snapshotLoading, setSnapshotLoading] = useState(false);
  const [detailLoading, setDetailLoading] = useState(false);
  const [brandLoading, setBrandLoading] = useState(false);
  const [opsLoading, setOpsLoading] = useState(false);
  const boardLoadId = useRef(0);
  const signedIn = Boolean(session);

  useEffect(() => {
    supabase.auth.getSession().then(({ data }) => setSession(data.session));
    const { data } = supabase.auth.onAuthStateChange((event, next) => {
      if (event === "TOKEN_REFRESHED" || event === "USER_UPDATED") return;
      setSession(next);
    });
    return () => data.subscription.unsubscribe();
  }, []);

  useEffect(() => {
    const onPopState = () => {
      const nextRoute = parseRoute();
      setRoute(nextRoute);
      setFiltersState(nextRoute.filters);
      setBrandFiltersState(nextRoute.brandFilters);
    };
    window.addEventListener("popstate", onPopState);
    return () => window.removeEventListener("popstate", onPopState);
  }, []);

  useEffect(() => {
    if (signedIn) loadNiches();
  }, [signedIn]);

  useEffect(() => {
    if (signedIn && (route.page === "board" || route.page === "ops")) loadTierSnapshot(filters.niche);
  }, [signedIn, route.page, filters.niche]);

  useEffect(() => {
    if (signedIn && route.page === "board") loadBoardRows();
  }, [signedIn, route.page, filters]);

  useEffect(() => {
    const niche = filters.niche || defaultNiche(niches, runnerStatus?.configs ?? []);
    if (signedIn && route.page === "ops" && niche) loadOpsSnapshot(niche);
  }, [signedIn, route.page, filters.niche, niches, runnerStatus?.configs]);

  const selected = useMemo(
    () => detailRow ?? rows.find((row) => (
      row.channel_id === route.channelId && (!route.niche || row.niche_config === route.niche)
    )) ?? null,
    [detailRow, rows, route.channelId, route.niche],
  );

  useEffect(() => {
    if (route.page === "creator" && route.channelId) {
      loadCreatorDetail(route.channelId, route.niche);
      loadCreator(route.channelId);
    }
  }, [route.page, route.channelId, route.niche]);

  useEffect(() => {
    if (route.page === "brands") loadBrandLinks();
  }, [route.page, brandFilters]);

  useEffect(() => {
    if (route.page === "brand" && route.domain) loadBrandDetail(route.domain);
  }, [route.page, route.domain, brandFilters]);

  useEffect(() => {
    if (route.page === "ops") loadOpsData();
  }, [route.page]);

  useEffect(() => {
    if (route.page !== "ops") return;
    checkRunner();
    const timer = window.setInterval(() => {
      checkRunner();
      if (runnerStatus?.running) loadOpsData();
    }, 3000);
    return () => window.clearInterval(timer);
  }, [route.page, runnerStatus?.running]);

  useEffect(() => {
    if (route.page === "creator") checkRunner();
  }, [route.page]);

  function navigate(path: string) {
    window.history.pushState({}, "", path);
    const nextRoute = parseRoute();
    setRoute(nextRoute);
    setFiltersState(nextRoute.filters);
    setBrandFiltersState(nextRoute.brandFilters);
  }

  function setFilters(nextFilters: Filters) {
    setFiltersState(nextFilters);
    if (route.page === "board") {
      const path = `/${boardSearch(nextFilters)}`;
      window.history.replaceState({}, "", path);
      setRoute(parseRoute());
    }
  }

  function setBrandFilters(nextFilters: BrandFilters) {
    setBrandFiltersState(nextFilters);
    const suffix = brandsSearch(nextFilters);
    const path = route.page === "brand" && route.domain
      ? `/brands/${encodeURIComponent(route.domain)}${suffix}`
      : `/brands${suffix}`;
    window.history.replaceState({}, "", path);
    const nextRoute = parseRoute();
    setRoute(nextRoute);
    setBrandFiltersState(nextRoute.brandFilters);
  }

  async function loadNiches() {
    try {
      setNiches(await fetchAllNicheNames());
    } catch {
      setNiches([]);
    }
  }

  async function loadTierSnapshot(niche = filters.niche) {
    setSnapshotLoading(true);
    try {
      setTierSnapshot(await fetchTierSnapshot(niche));
    } catch {
      setTierSnapshot([]);
    } finally {
      setSnapshotLoading(false);
    }
  }

  async function loadOpsSnapshot(niche: string) {
    setSnapshotLoading(true);
    try {
      setOpsSnapshot(await fetchOpsSnapshot(niche));
    } catch {
      setOpsSnapshot({ total: 0, discovered: 0, stage1: 0, vetted: 0, disqualified: 0 });
    } finally {
      setSnapshotLoading(false);
    }
  }

  async function refreshBoard() {
    await Promise.all([loadNiches(), loadTierSnapshot(filters.niche), loadBoardRows()]);
  }

  async function loadBoardRows() {
    const loadId = boardLoadId.current + 1;
    boardLoadId.current = loadId;
    setLoading(true);

    let query = supabase
      .from("creator_niches")
      .select("*, creators(*)")
      .order("vetting_score", { ascending: false, nullsFirst: false })
      .limit(500);
    if (filters.niche) query = query.eq("niche_config", filters.niche);
    if (filters.status) query = query.eq("vetting_status", filters.status);
    const result = await query;
    if (loadId !== boardLoadId.current) return;
    let nextRows = (result.data ?? []) as CreatorNiche[];
    if (filters.tier) nextRows = nextRows.filter((row) => row.creators?.tier === filters.tier);
    if (filters.q.trim()) {
      const needle = filters.q.toLowerCase();
      nextRows = nextRows.filter((row) => {
        const creator = row.creators;
        return [creator?.display_name, creator?.channel_id, creator?.description_raw, row.discovery_keyword]
          .some((value) => (value ?? "").toLowerCase().includes(needle));
      });
    }
    setRows(nextRows);
    loadBoardMetrics(nextRows, loadId);
    setLoading(false);
  }

  async function loadBoardMetrics(nextRows: CreatorNiche[], loadId = boardLoadId.current) {
    const channelIds = Array.from(new Set(nextRows.map((row) => row.channel_id))).slice(0, 500);
    if (!channelIds.length) {
      if (loadId !== boardLoadId.current) return;
      setBoardMetrics({});
      return;
    }
    const result = await supabase
      .from("videos")
      .select("*")
      .in("channel_id", channelIds)
      .order("published_at", { ascending: false })
      .limit(5000);
    const grouped = new Map<string, Video[]>();
    for (const video of (result.data ?? []) as Video[]) {
      const key = (video as Video & { channel_id?: string }).channel_id;
      if (!key) continue;
      grouped.set(key, [...(grouped.get(key) ?? []), video]);
    }
    const nextMetrics: Record<string, DerivedMetrics> = {};
    for (const row of nextRows) {
      const videos = grouped.get(row.channel_id);
      if (!videos?.length) continue;
      nextMetrics[row.channel_id] = computeCreatorMetrics(videos, Number(row.creators?.subscriber_count ?? 0));
    }
    if (loadId !== boardLoadId.current) return;
    setBoardMetrics(nextMetrics);
  }

  async function loadCreatorDetail(channelId: string, niche = "") {
    setDetailLoading(true);
    let query = supabase
      .from("creator_niches")
      .select("*, creators(*)")
      .eq("channel_id", channelId)
      .limit(1);
    if (niche) query = query.eq("niche_config", niche);
    const result = await query;
    setDetailRow((result.data?.[0] as CreatorNiche | undefined) ?? null);
    setDetailLoading(false);
  }

  async function loadCreator(channelId: string) {
    const [videoResult, linkResult] = await Promise.all([
      supabase.from("videos").select("*").eq("channel_id", channelId).order("published_at", { ascending: false }).limit(200),
      supabase.from("outbound_links").select("*").eq("channel_id", channelId).order("detected_at", { ascending: false }).limit(150),
    ]);
    setVideos((videoResult.data ?? []) as Video[]);
    setLinks((linkResult.data ?? []) as LinkRow[]);
  }

  async function loadBrandLinks() {
    setBrandLoading(true);
    try {
      const [summary, exports] = await Promise.all([
        fetchBrandSummary(brandFilters),
        fetchBrandExports(brandFilters.niche),
      ]);
      setBrandLinks(mergeBrandExports(summary, exports));
    } catch {
      setBrandLinks([]);
    }
    setBrandLoading(false);
  }

  async function loadBrandDetail(domain: string) {
    setBrandLoading(true);
    try {
      const [rows, exports] = await Promise.all([
        fetchBrandDetail(domain, brandFilters),
        fetchBrandExports(brandFilters.niche),
      ]);
      setBrandDetailCrm(exports.find((row) => row.domain === domain) ?? null);
      setBrandDetailLinks(rows.map((row) => ({
        id: row.id,
        channel_id: row.channel_id,
        domain: row.domain,
        source_url: row.source_url,
        evidence_text: row.evidence_text,
        video_id: row.video_id,
        detected_at: row.detected_at,
        mention_type: row.mention_type,
        creators: row.channel_id ? {
          channel_id: row.channel_id,
          handle: null,
          display_name: row.creator_display_name,
          channel_url: null,
          description_raw: null,
          country: null,
          subscriber_count: row.creator_subscriber_count,
          total_view_count: null,
          video_count: null,
          tier: row.creator_tier,
          uploads_per_month: null,
          upload_frequency_label: null,
          sampled_video_count: null,
          sampled_long_video_count: null,
          sampled_short_video_count: null,
          content_mix: null,
          avg_long_views: null,
          avg_short_views: null,
          engagement_rate_per_view_long: null,
          engagement_rate_per_sub_long: null,
          engagement_rate_per_view_short: null,
          engagement_rate_per_sub_short: null,
          contact_email: null,
          social_links: null,
        } : null,
        videos: row.video_id ? {
          video_id: row.video_id,
          channel_id: row.channel_id,
          title: row.video_title,
          description_raw: null,
          published_at: null,
          duration_seconds: null,
          is_short: false,
          view_count: row.video_view_count,
          like_count: null,
          comment_count: null,
          video_url: row.video_url,
        } : null,
      })));
    } catch {
      setBrandDetailLinks([]);
      setBrandDetailCrm(null);
    }
    setBrandLoading(false);
  }

  async function loadOpsData() {
    setOpsLoading(true);
    const today = new Date().toISOString().slice(0, 10);
    const [runsResult, nextQuotaLogs, keywordsResult] = await Promise.all([
      supabase
        .from("pipeline_runs")
        .select("*")
        .order("started_at", { ascending: false })
        .limit(20),
      fetchQuotaLogsForDate(today),
      supabase
        .from("search_keywords")
        .select("*")
        .order("niche_config", { ascending: true })
        .order("keyword", { ascending: true })
        .limit(1000),
    ]);
    setPipelineRuns((runsResult.data ?? []) as PipelineRun[]);
    setQuotaLogs(nextQuotaLogs);
    setSearchKeywords((keywordsResult.data ?? []) as SearchKeyword[]);
    setOpsLoading(false);
  }

  async function checkRunner() {
    try {
      const response = await fetch(`${LOCAL_RUNNER_URL}/status`, { cache: "no-store" });
      const data = await response.json();
      setRunnerStatus(data as RunnerStatus);
    } catch {
      setRunnerStatus({
        ok: false,
        running: false,
        current: null,
        last: null,
        configs: [],
        error: "Local runner is not connected.",
      });
    }
  }

  async function startRunnerStage(stage: string, config: string, options: { recent_videos_to_sample?: number } = {}) {
    setRunnerBusy(true);
    try {
      const response = await fetch(`${LOCAL_RUNNER_URL}/run`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ stage, config, ...options }),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error ?? "Could not start runner");
      await checkRunner();
      await loadOpsData();
    } catch (error) {
      setRunnerStatus({
        ok: false,
        running: false,
        current: null,
        last: null,
        configs: runnerStatus?.configs ?? [],
        error: error instanceof Error ? error.message : "Could not start runner",
      });
    } finally {
      setRunnerBusy(false);
    }
  }

  async function stopRunner() {
    setRunnerBusy(true);
    try {
      const response = await fetch(`${LOCAL_RUNNER_URL}/stop`, { method: "POST" });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error ?? "Could not stop runner");
      await checkRunner();
    } catch (error) {
      setRunnerStatus({
        ok: false,
        running: false,
        current: runnerStatus?.current ?? null,
        last: runnerStatus?.last ?? null,
        configs: runnerStatus?.configs ?? [],
        error: error instanceof Error ? error.message : "Could not stop runner",
      });
    } finally {
      setRunnerBusy(false);
    }
  }

  async function saveReview(values: Partial<CreatorNiche>) {
    if (!selected || !session) return;
    setSaving(true);
    const result = await supabase
      .from("creator_niches")
      .update({ ...values, reviewed_by: session.user.id, reviewed_at: new Date().toISOString() })
      .eq("niche_config", selected.niche_config)
      .eq("channel_id", selected.channel_id)
      .select("*, creators(*)")
      .single();
    setSaving(false);
    if (result.data) {
      setDetailRow(result.data as CreatorNiche);
      setRows((current) => current.map((row) => (
        row.niche_config === selected.niche_config && row.channel_id === selected.channel_id
          ? result.data as CreatorNiche
          : row
      )));
    }
  }

  async function sendCreatorToAirtable(row: CreatorNiche, contactEmail: string | null, sponsoredBrandDomains: string[]) {
    const busyKey = `creator:${row.niche_config}:${row.channel_id}`;
    setAirtableBusy(busyKey);
    try {
      const result = await supabase.functions.invoke("send-to-airtable", {
        body: {
          kind: "creator",
          channelId: row.channel_id,
          nicheConfig: row.niche_config,
          contactEmail,
          sponsoredBrandDomains,
        },
      });
      if (result.error) throw result.error;
      await loadCreatorDetail(row.channel_id, row.niche_config);
      await loadBoardRows();
    } finally {
      setAirtableBusy("");
    }
  }

  async function sendBrandToAirtable(domain: string, nicheConfig = brandFilters.niche) {
    const busyKey = `brand:${nicheConfig}:${domain}`;
    setAirtableBusy(busyKey);
    try {
      const result = await supabase.functions.invoke("send-to-airtable", {
        body: { kind: "brand", domain, nicheConfig },
      });
      if (result.error) throw result.error;
      if (route.page === "brands") await loadBrandLinks();
      if (route.page === "brand") await loadBrandDetail(domain);
    } finally {
      setAirtableBusy("");
    }
  }

  async function clearCreatorCrmMark(row: CreatorNiche) {
    const busyKey = `creator:${row.niche_config}:${row.channel_id}`;
    setAirtableBusy(busyKey);
    try {
      const result = await supabase.functions.invoke("send-to-airtable", {
        body: { kind: "clear_creator", channelId: row.channel_id, nicheConfig: row.niche_config },
      });
      if (result.error) throw result.error;
      await loadCreatorDetail(row.channel_id, row.niche_config);
      await loadBoardRows();
    } finally {
      setAirtableBusy("");
    }
  }

  async function clearBrandCrmMark(domain: string, nicheConfig = brandFilters.niche) {
    const busyKey = `brand:${nicheConfig}:${domain}`;
    setAirtableBusy(busyKey);
    try {
      const result = await supabase.functions.invoke("send-to-airtable", {
        body: { kind: "clear_brand", domain, nicheConfig },
      });
      if (result.error) throw result.error;
      if (route.page === "brands") await loadBrandLinks();
      if (route.page === "brand") await loadBrandDetail(domain);
    } finally {
      setAirtableBusy("");
    }
  }

  if (!session) return <AuthGate onSession={setSession} />;

  return (
    <>
      {route.page === "board" ? (
        <BoardPage
          rows={rows}
          tierSnapshot={tierSnapshot}
          niches={niches}
          filters={filters}
          loading={loading}
          snapshotLoading={snapshotLoading}
          boardMetrics={boardMetrics}
          setFilters={setFilters}
          openCreator={(row) => {
            const returnTo = `${window.location.pathname}${window.location.search}`;
            navigate(`/creator/${encodeURIComponent(row.channel_id)}?${new URLSearchParams({
              niche: row.niche_config,
              return_to: returnTo,
            }).toString()}`);
          }}
          openBrands={() => navigate(`/brands${brandsSearch({ niche: filters.niche, q: "", mentionType: "sponsored" })}`)}
          openOps={() => navigate("/ops")}
          refresh={refreshBoard}
          signOut={() => supabase.auth.signOut()}
        />
      ) : null}
      {route.page === "creator" && selected ? (
        <CreatorPage
          row={selected}
          videos={videos}
          links={links}
          saving={saving}
          airtableBusy={airtableBusy === `creator:${selected.niche_config}:${selected.channel_id}`}
          loading={detailLoading}
          runnerConnected={Boolean(runnerStatus?.ok)}
          back={() => navigate(route.returnTo || "/")}
          saveReview={saveReview}
          sendToAirtable={sendCreatorToAirtable}
          clearCrmMark={clearCreatorCrmMark}
          refresh={() => loadCreator(selected.channel_id)}
          fetchLatest={() => startRunnerStage("stage2", runnerStatus?.configs.find((config) => config.name === selected.niche_config)?.path ?? `config/niche_${selected.niche_config}.yaml`, { recent_videos_to_sample: 50 })}
        />
      ) : null}
      {route.page === "brands" ? (
        <BrandsPage
          links={brandLinks}
          niches={niches}
          filters={brandFilters}
          loading={brandLoading}
          setFilters={setBrandFilters}
          back={() => navigate(`/${boardSearch(filters)}`)}
          openBrand={(domain) => navigate(`/brands/${encodeURIComponent(domain)}${brandsSearch(brandFilters)}`)}
        />
      ) : null}
      {route.page === "brand" && route.domain ? (
        <BrandDetailPage
          domain={route.domain}
          links={brandDetailLinks}
          crm={brandDetailCrm}
          loading={brandLoading}
          airtableBusy={airtableBusy === `brand:${brandFilters.niche}:${route.domain}`}
          back={() => navigate(`/brands${brandsSearch(brandFilters)}`)}
          openCreator={(channelId) => navigate(`/creator/${encodeURIComponent(channelId)}?${new URLSearchParams({ return_to: `/brands/${encodeURIComponent(route.domain ?? "")}${brandsSearch(brandFilters)}` }).toString()}`)}
          setFilters={setBrandFilters}
          filters={brandFilters}
          sendToAirtable={() => sendBrandToAirtable(route.domain ?? "", brandFilters.niche)}
          clearCrmMark={() => clearBrandCrmMark(route.domain ?? "", brandFilters.niche)}
        />
      ) : null}
      {route.page === "ops" ? (
        <OpsPage
          rows={rows}
          opsSnapshot={opsSnapshot}
          niches={niches}
          filters={filters}
          runs={pipelineRuns}
          quotaLogs={quotaLogs}
          searchKeywords={searchKeywords}
          runnerStatus={runnerStatus}
          runnerBusy={runnerBusy}
          snapshotLoading={snapshotLoading}
          loading={opsLoading}
          setFilters={setFilters}
          startRunnerStage={startRunnerStage}
          stopRunner={stopRunner}
          checkRunner={checkRunner}
          refresh={() => {
            loadNiches();
            loadTierSnapshot(filters.niche);
            loadOpsSnapshot(filters.niche || defaultNiche(niches, runnerStatus?.configs ?? []));
            loadOpsData();
            checkRunner();
          }}
          back={() => navigate(`/${boardSearch(filters)}`)}
        />
      ) : null}
    </>
  );
}

function BoardPage(props: {
  rows: CreatorNiche[];
  tierSnapshot: TierSnapshotRow[];
  niches: string[];
  filters: Filters;
  loading: boolean;
  snapshotLoading: boolean;
  boardMetrics: Record<string, DerivedMetrics>;
  setFilters: (filters: Filters) => void;
  openCreator: (row: CreatorNiche) => void;
  openBrands: () => void;
  openOps: () => void;
  refresh: () => void;
  signOut: () => void;
}) {
  const { rows, niches, filters, setFilters } = props;
  const visibleRows = useMemo(() => {
    return [...rows].sort((a, b) => {
      const aCreator = a.creators;
      const bCreator = b.creators;
      const aMetrics = props.boardMetrics[a.channel_id];
      const bMetrics = props.boardMetrics[b.channel_id];
      if (filters.sort === "subscribers") {
        return Number(bCreator?.subscriber_count ?? 0) - Number(aCreator?.subscriber_count ?? 0);
      }
      if (filters.sort === "er_view") {
        return Number(bMetrics?.longMetrics.engagementPerView ?? bCreator?.engagement_rate_per_view_long ?? 0)
          - Number(aMetrics?.longMetrics.engagementPerView ?? aCreator?.engagement_rate_per_view_long ?? 0);
      }
      if (filters.sort === "er_sub") {
        return Number(bMetrics?.longMetrics.engagementPerSub ?? bCreator?.engagement_rate_per_sub_long ?? 0)
          - Number(aMetrics?.longMetrics.engagementPerSub ?? aCreator?.engagement_rate_per_sub_long ?? 0);
      }
      return Number(bMetrics?.score ?? b.vetting_score ?? 0) - Number(aMetrics?.score ?? a.vetting_score ?? 0);
    });
  }, [rows, filters.sort, props.boardMetrics]);
  const totalCreators = props.tierSnapshot.reduce((sum, row) => sum + Number(row.total ?? 0), 0);
  const totalVetted = props.tierSnapshot.reduce((sum, row) => sum + Number(row.vetted ?? 0), 0);
  const hasSnapshot = props.tierSnapshot.length > 0;
  const statValue = (value: number) => props.snapshotLoading && !hasSnapshot ? "-" : fmtInt(value);
  const statSubvalue = (value: number, noun = "vetted") => props.snapshotLoading ? "updating..." : `${fmtInt(value)} ${noun}`;

  return (
    <>
      <Hero title="Creator review board" copy="Review creators by niche, score, vetting status, and fit notes. Sort by score, engagement, or subscribers.">
        <button className="btn secondary" onClick={() => setFilters({ niche: "", status: "vetted", tier: "", q: "", sort: "score" })}>Reset filters</button>
        <button className="btn soft" onClick={props.openBrands}>Brands</button>
        <button className="btn" onClick={props.openOps}>Pipeline</button>
        <button className="btn" onClick={props.refresh}><RefreshCw size={15} />Refresh</button>
        <button className="btn" onClick={props.signOut}><LogOut size={15} />Sign out</button>
      </Hero>
      <main>
        <section className="card">
          <div className="card-head"><h2>Tier snapshot</h2><p>Only the niche filter changes this snapshot.</p></div>
          <div className="grid">
            <Stat label="Total" value={statValue(totalCreators)} subvalue={statSubvalue(totalVetted)} tone="strong" />
            {props.snapshotLoading && !hasSnapshot ? TIER_ORDER.map((tier) => <Stat key={tier} label={tier} value="-" subvalue="loading..." />) : props.tierSnapshot.map((count) => <Stat key={count.tier} label={count.tier} value={fmtInt(count.total)} subvalue={props.snapshotLoading ? "updating..." : `${fmtInt(count.vetted)} vetted`} />)}
          </div>
        </section>
        <section className="card">
          <div className="card-head"><h2>Filters</h2><p>Vetted creators are shown by default. Niche also updates the tier snapshot.</p></div>
          <div className="controls">
            <div><div className="label">Search</div><input className="field" type="search" value={filters.q} onChange={(event) => setFilters({ ...filters, q: event.target.value })} placeholder="Creator name, channel id, keyword" /></div>
            <div><div className="label">Niche</div><select className="field" value={filters.niche} onChange={(event) => setFilters({ ...filters, niche: event.target.value })}><option value="">All niches</option>{niches.map((niche) => <option key={niche} value={niche}>{niche}</option>)}</select></div>
            <div><div className="label">Vetting status</div><select className="field" value={filters.status} onChange={(event) => setFilters({ ...filters, status: event.target.value })}><option value="">All statuses</option>{VETTING_STATUSES.map((status) => <option key={status} value={status}>{label(status)}</option>)}</select></div>
            <div><div className="label">Tier</div><select className="field" value={filters.tier} onChange={(event) => setFilters({ ...filters, tier: event.target.value })}><option value="">All tiers</option>{TIER_ORDER.map((tier) => <option key={tier} value={tier}>{tier}</option>)}</select></div>
            <div><div className="label">Sort by</div><select className="field" value={filters.sort} onChange={(event) => setFilters({ ...filters, sort: event.target.value as SortKey })}><option value="score">Score</option><option value="er_view">Long ER / view</option><option value="er_sub">Long ER / sub</option><option value="subscribers">Subscribers</option></select></div>
          </div>
        </section>
        <Collapsible title="Creator board" subtitle={props.loading ? `${visibleRows.length} rows visible. Updating...` : `${visibleRows.length} rows visible.`} defaultOpen>
          <div className="table-wrap">
            <table className="creator-table">
              <thead><tr><th>Creator</th><th>Country</th><th>Niche / vetting</th><th>Fit</th><th>CRM</th><th>Subscribers</th><th>Videos</th><th>Score</th><th>ER / view</th><th>Notes</th></tr></thead>
              <tbody>
                {(!props.loading || visibleRows.length) ? visibleRows.map((row) => {
                  const creator = row.creators;
                  const metrics = props.boardMetrics[row.channel_id];
                  const sampledTotal = Number(creator?.sampled_video_count ?? 0);
                  const storedLongCount = Number(creator?.sampled_long_video_count ?? 0);
                  const storedShortCount = Number(creator?.sampled_short_video_count ?? 0);
                  const storedVideoCount = storedLongCount + storedShortCount;
                  const metricsVideoCount = metrics ? metrics.longMetrics.count + metrics.shortMetrics.count : 0;
                  const visibleVideoCount = sampledTotal || storedVideoCount || metricsVideoCount;
                  const breakdownMatchesTotal = storedVideoCount > 0 && (!sampledTotal || storedVideoCount === sampledTotal);
                  const labels = [row.fit_label].filter(Boolean);
                  return (
                    <tr key={`${row.niche_config}:${row.channel_id}`}>
                      <td><button className="link-button" onClick={() => props.openCreator(row)}><strong>{creator?.display_name ?? row.channel_id}</strong></button></td>
                      <td>{creator?.country ?? "-"}</td>
                      <td><Badge>{row.niche_config}</Badge><Badge tone={badgeTone("status", row.vetting_status)}>{label(row.vetting_status)}</Badge></td>
                      <td>{labels.length ? labels.map((item) => <Badge key={item} tone={badgeTone("fit", item)}>{label(item)}</Badge>) : <span className="muted small">No fit label</span>}</td>
                      <td>{row.airtable_creator_synced_at ? <Badge tone="strong">sent</Badge> : <Badge>not sent</Badge>}</td>
                      <td>{fmtInt(creator?.subscriber_count)}<div className="muted small">{creator?.tier ?? "unknown"}</div></td>
                      <td>{visibleVideoCount ? fmtInt(visibleVideoCount) : "-"}<div className="muted small">{visibleVideoCount ? (breakdownMatchesTotal ? `${storedLongCount} long / ${storedShortCount} short` : "cached videos") : "none cached"}</div></td>
                      <td><span className={`metric-${badgeTone("score", metrics?.score ?? row.vetting_score)}`}>{metrics?.score == null ? (row.vetting_score == null ? "-" : fmtFloat(row.vetting_score)) : fmtFloat(metrics.score)}</span></td>
                      <td><span className="metric-ok">{metrics ? fmtPct(metrics.longMetrics.engagementPerView) : fmtPct(creator?.engagement_rate_per_view_long)}</span></td>
                      <td><div className="note-preview">{truncate(row.review_notes ?? row.disqualified_reason)}</div></td>
                    </tr>
                  );
                }) : <tr><td colSpan={10} className="muted">Loading creators...</td></tr>}
                {!props.loading && rows.length === 0 ? <tr><td colSpan={10} className="muted">No creators matched those filters.</td></tr> : null}
              </tbody>
            </table>
          </div>
        </Collapsible>
      </main>
    </>
  );
}

function CreatorPage(props: {
  row: CreatorNiche;
  videos: Video[];
  links: LinkRow[];
  saving: boolean;
  airtableBusy: boolean;
  loading: boolean;
  runnerConnected: boolean;
  back: () => void;
  refresh: () => void;
  fetchLatest: () => void;
  saveReview: (values: Partial<CreatorNiche>) => void;
  sendToAirtable: (row: CreatorNiche, contactEmail: string | null, brandDomains: string[]) => Promise<void>;
  clearCrmMark: (row: CreatorNiche) => Promise<void>;
}) {
  const creator = props.row.creators;
  const [notes, setNotes] = useState(props.row.review_notes ?? "");
  const [airtableMessage, setAirtableMessage] = useState("");
  useEffect(() => setNotes(props.row.review_notes ?? ""), [props.row.review_notes]);
  if (!creator) return null;
  const derived = computeCreatorMetrics(props.videos, Number(creator.subscriber_count ?? 0));
  const brandedLinks = props.links.map((link) => ({ ...link, mention_type: classifyLink(link) }));
  const fullText = [creator.description_raw, ...props.videos.flatMap((video) => [video.title, video.description_raw])].filter(Boolean).join(" ");
  const contactEmail = creator.contact_email || extractEmailFromText(fullText);
  const socialLinks = { ...extractSocialLinksFromText(fullText), ...parseSocialLinks(creator.social_links) };
  const brandSummary = Array.from(brandedLinks.reduce((map, link) => {
    if (link.mention_type === "owned") return map;
    const item = map.get(link.domain) ?? { domain: link.domain, sponsored: 0, affiliate: 0, link: 0, total: 0 };
    item[link.mention_type as "sponsored" | "affiliate" | "link"] += 1;
    item.total += 1;
    map.set(link.domain, item);
    return map;
  }, new Map<string, { domain: string; sponsored: number; affiliate: number; link: number; total: number }>()).values()).slice(0, 24);
  const alreadySent = Boolean(props.row.airtable_creator_record_id);

  async function sendCreator() {
    setAirtableMessage("");
    try {
      await props.sendToAirtable(props.row, contactEmail, brandSummary.filter((brand) => brand.sponsored > 0).map((brand) => brand.domain));
      setAirtableMessage("Creator sent to Airtable.");
    } catch (error) {
      setAirtableMessage(error instanceof Error ? error.message : "Could not send creator to Airtable.");
    }
  }

  async function clearCreatorMark() {
    setAirtableMessage("");
    try {
      await props.clearCrmMark(props.row);
      setAirtableMessage("Creator CRM mark cleared.");
    } catch (error) {
      setAirtableMessage(error instanceof Error ? error.message : "Could not clear CRM mark.");
    }
  }

  return (
    <>
      <Hero title={creator.display_name ?? creator.channel_id} copy={creator.channel_id}>
        <button className="btn secondary" disabled={props.airtableBusy} onClick={sendCreator}>
          {alreadySent ? <CheckCircle2 size={15} /> : <Send size={15} />}
          {alreadySent ? "Update CRM" : "Send to CRM"}
        </button>
        {alreadySent ? <button className="btn soft" disabled={props.airtableBusy} onClick={clearCreatorMark}>Unmark CRM</button> : null}
        <button className="btn soft" onClick={props.refresh}>Refresh stored videos</button>
        <button className="btn soft" disabled={!props.runnerConnected} onClick={props.fetchLatest}>Fetch latest 50 videos</button>
        <button className="btn secondary" onClick={props.back}>Back to board</button>
        {creator.channel_url ? <a className="btn" href={creator.channel_url} target="_blank" rel="noreferrer">Open channel <ExternalLink size={15} /></a> : null}
      </Hero>
      <main>
        <section className="card creator-meta">
          <div className="stack">
            <Badge tone="sky">Country: {creator.country ?? "-"}</Badge>
            <Badge tone="sky">Mix: {derived.contentMix}</Badge>
            <Badge tone={badgeTone("status", props.row.vetting_status)}>{props.row.niche_config} / {label(props.row.vetting_status)}</Badge>
            {props.row.airtable_creator_synced_at ? <Badge tone="strong">CRM sent {fmtDate(props.row.airtable_creator_synced_at, true)}</Badge> : <Badge>CRM not sent</Badge>}
          </div>
          <div className="creator-card-grid">
            <Stat label="Subscribers" value={fmtInt(creator.subscriber_count)} subvalue={creator.tier ?? "unknown"} />
            <Stat label="Score" value={derived.score == null ? "-" : fmtFloat(derived.score)} subvalue="best available score" tone={badgeTone("score", derived.score)} />
            <Stat label="Uploads / month" value={fmtFloat(derived.uploadsPerMonth, 2)} subvalue={derived.uploadFrequencyLabel} />
            <Stat label="Cached videos" value={fmtInt(props.videos.length)} subvalue={`${fmtInt(derived.longMetrics.count)} long / ${fmtInt(derived.shortMetrics.count)} short`} />
            <Stat label="Avg long views" value={fmtInt(derived.longMetrics.avgViews)} subvalue={`${fmtPct(derived.longMetrics.engagementPerView)} ER / view`} />
            <Stat label="Long ER / view" value={fmtPct(derived.longMetrics.engagementPerView)} subvalue="likes + comments / views" />
            <Stat label="Long ER / sub" value={fmtPct(derived.longMetrics.engagementPerSub)} subvalue="likes + comments / subscribers" />
            <Stat label="Avg short views" value={fmtInt(derived.shortMetrics.avgViews)} subvalue={`${fmtPct(derived.shortMetrics.engagementPerView)} ER / view`} />
          </div>
          {airtableMessage ? <div className={`notice ${airtableMessage.includes("sent") || airtableMessage.includes("cleared") ? "ok" : "error"}`}>{airtableMessage}</div> : null}
          {props.row.airtable_creator_sync_error ? <div className="notice error">Last CRM error: {props.row.airtable_creator_sync_error}</div> : null}
          {props.row.disqualified_reason ? <div className="notice error">{props.row.disqualified_reason}</div> : null}
        </section>
        <section className="split creator-body">
          <div className="section-stack">
            <div className="card">
              <div className="card-head"><h2>Contact and creator links</h2></div>
              <div className="panel-item"><div className="label">Contact email</div><div className="contact-line">{contactEmail ?? "No email found yet"}</div></div>
              <div className="stack">{Object.keys(socialLinks).length ? Object.entries(socialLinks).map(([name, url]) => <a key={name} className="badge sky" href={String(url).startsWith("http") ? String(url) : `https://${url}`} target="_blank" rel="noreferrer">{name}</a>) : <span className="muted">No social links detected yet.</span>}</div>
              <div className="panel-item description-box"><div className="label">Channel description</div><div className="muted prewrap">{creator.description_raw ?? ""}</div></div>
            </div>
          </div>
          <div className="section-stack creator-sidebar">
            <div className="card qualification-card">
              <div className="card-head"><h2>Qualification</h2><p>Review fit and leave internal notes before Airtable outreach exists.</p></div>
              <div>
                <div className="label">Fit label</div>
                <div className="choice-row">
                  {FIT_LABELS.map((fit) => {
                    const selected = props.row.fit_label === fit;
                    return (
                      <button
                        key={fit}
                        type="button"
                        className={`choice-badge ${selected ? "selected" : ""}`}
                        onClick={() => props.saveReview({ fit_label: selected ? null : fit })}
                      >
                        {label(fit)}
                      </button>
                    );
                  })}
                </div>
              </div>
              <div className="mt-4"><div className="label">Notes</div><textarea className="field" value={notes} onChange={(event) => setNotes(event.target.value)} /></div>
              <div className="mt-4 stack"><button className="btn" disabled={props.saving} onClick={() => props.saveReview({ review_notes: notes })}>Save review notes</button></div>
            </div>
          </div>
        </section>
        <section className="card">
          <div className="card-head"><h2>Brand history</h2><p>Sponsored, affiliate, and other non-owned outbound links for this creator.</p></div>
          <div className="brand-history-grid">
            {brandSummary.map((brand) => <div key={brand.domain} className="panel-item"><div className="topline"><strong>{brand.domain}</strong><Badge>{brand.total} brand-like</Badge></div><div className="stack"><Badge tone="strong">{brand.sponsored} sponsored</Badge><Badge tone="ok">{brand.affiliate} affiliate</Badge><Badge>{brand.link} links</Badge></div></div>)}
            {brandSummary.length === 0 ? <div className="muted">No non-owned brand links found yet.</div> : null}
          </div>
        </section>
        <TableCard title="Recent videos" subtitle={`Showing ${props.videos.length} stored videos`} columns={["Video", "Published", "Format", "Views", "Likes", "Comments", "ER / View", "ER / Sub"]}>
          {props.videos.map((video) => {
            const engagement = Number(video.like_count ?? 0) + Number(video.comment_count ?? 0);
            const engagementPerView = video.view_count ? engagement / Number(video.view_count) : 0;
            const engagementPerSub = creator.subscriber_count ? engagement / Number(creator.subscriber_count) : 0;
            return <tr key={video.video_id}><td><a href={video.video_url ?? "#"} target="_blank" rel="noreferrer">{video.title ?? video.video_id}</a></td><td>{fmtDate(video.published_at)}</td><td>{video.is_short ? "short" : "long"}</td><td>{fmtInt(video.view_count)}</td><td>{fmtInt(video.like_count)}</td><td>{fmtInt(video.comment_count)}</td><td>{fmtPct(engagementPerView)}</td><td>{fmtPct(engagementPerSub)}</td></tr>;
          })}
        </TableCard>
        <TableCard title="Recent link evidence" subtitle="Raw evidence behind the brand detection." columns={["Domain", "Type", "URL", "Evidence"]}>
          {brandedLinks.map((link) => <tr key={link.id}><td>{link.domain}</td><td className="type-cell"><Badge tone={badgeTone("link", link.mention_type)}>{link.mention_type}</Badge></td><td><a href={link.source_url} target="_blank" rel="noreferrer">open link</a></td><td>{link.evidence_text ?? "-"}</td></tr>)}
        </TableCard>
      </main>
    </>
  );
}

function BrandsPage(props: {
  links: BrandSummaryRow[];
  niches: string[];
  filters: BrandFilters;
  loading: boolean;
  setFilters: (filters: BrandFilters) => void;
  back: () => void;
  openBrand: (domain: string) => void;
}) {
  const brands = useMemo(() => {
    return props.links
      .sort((a, b) => b.sponsored - a.sponsored || b.affiliate - a.affiliate || b.creator_count - a.creator_count || a.domain.localeCompare(b.domain));
  }, [props.links]);

  return (
    <>
      <Hero title="Brand explorer" copy="Detected non-owned brand links. Filter by niche and mention type.">
        <button className="btn secondary" onClick={props.back}>Back to board</button>
      </Hero>
      <main>
        <section className="card">
          <div className="card-head"><h2>Brand filters</h2></div>
          <div className="controls">
            <div><div className="label">Niche</div><select className="field" value={props.filters.niche} onChange={(event) => props.setFilters({ ...props.filters, niche: event.target.value })}><option value="">All niches</option>{props.niches.map((niche) => <option key={niche} value={niche}>{niche}</option>)}</select></div>
            <div><div className="label">Mention type</div><select className="field" value={props.filters.mentionType} onChange={(event) => props.setFilters({ ...props.filters, mentionType: event.target.value as BrandFilters["mentionType"] })}><option value="sponsored">Sponsored only</option><option value="affiliate">Affiliate only</option><option value="link">Links only</option><option value="all">All types</option></select></div>
            <div><div className="label">Search domain</div><input className="field" type="search" value={props.filters.q} onChange={(event) => props.setFilters({ ...props.filters, q: event.target.value })} placeholder="booking, amazon, nordvpn" /></div>
          </div>
        </section>
        <section className="card">
          <div className="card-head"><h2>Brands</h2><p>{props.loading ? "Loading outbound link evidence..." : `${brands.length} domains visible.`}</p></div>
          <div className="table-wrap"><table><thead><tr><th>Domain</th><th>Creators</th><th>Videos</th><th>Types</th><th>CRM</th><th>URL</th><th>Last seen</th></tr></thead><tbody>{brands.map((brand) => (
            <tr key={brand.domain}><td><button className="link-button" onClick={() => props.openBrand(brand.domain)}><strong>{brand.domain}</strong></button></td><td>{fmtInt(brand.creator_count)}</td><td>{fmtInt(brand.video_count)}</td><td className="type-cell"><Badge tone="strong">{brand.sponsored} sponsored</Badge><Badge tone="ok">{brand.affiliate} affiliate</Badge><Badge>{brand.link} links</Badge></td><td>{brand.airtable_brand_synced_at ? <Badge tone="strong">sent</Badge> : <Badge>not sent</Badge>}{brand.airtable_brand_sync_error ? <div className="muted small">{brand.airtable_brand_sync_error}</div> : null}</td><td><a href={`https://${brand.domain}`} target="_blank" rel="noreferrer">visit</a></td><td>{fmtDate(brand.last_seen, true)}</td></tr>
          ))}</tbody></table></div>
        </section>
      </main>
    </>
  );
}

function BrandDetailPage(props: {
  domain: string;
  links: LinkRow[];
  crm: BrandCrmExport | null;
  loading: boolean;
  airtableBusy: boolean;
  filters: BrandFilters;
  back: () => void;
  openCreator: (channelId: string) => void;
  setFilters: (filters: BrandFilters) => void;
  sendToAirtable: () => Promise<void>;
  clearCrmMark: () => Promise<void>;
}) {
  const [message, setMessage] = useState("");
  const classified = props.links.map((link) => ({ ...link, mention_type: classifyLink(link) }));
  const creators = Array.from(new Map(classified.map((link) => [link.channel_id, link])).values()).filter((link) => link.channel_id);
  const videos = Array.from(new Map(classified.map((link) => [link.video_id, link])).values()).filter((link) => link.video_id);

  async function sendBrand() {
    setMessage("");
    try {
      await props.sendToAirtable();
      setMessage(`${props.domain} sent to Airtable.`);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : "Could not send brand to Airtable.");
    }
  }

  async function clearBrandMark() {
    setMessage("");
    try {
      await props.clearCrmMark();
      setMessage(`${props.domain} CRM mark cleared.`);
    } catch (error) {
      setMessage(error instanceof Error ? error.message : "Could not clear CRM mark.");
    }
  }

  return (
    <>
      <Hero title={props.domain} copy="Brand detail view with matching creators, videos, and exact outbound links behind the match.">
        <button className="btn secondary" disabled={props.airtableBusy || Boolean(props.crm?.airtable_brand_record_id)} onClick={sendBrand}>
          {props.crm?.airtable_brand_record_id ? <CheckCircle2 size={15} /> : <Send size={15} />}
          {props.crm?.airtable_brand_record_id ? "Sent to CRM" : "Send to CRM"}
        </button>
        {props.crm?.airtable_brand_record_id ? <button className="btn soft" disabled={props.airtableBusy} onClick={clearBrandMark}>Unmark CRM</button> : null}
        <button className="btn secondary" onClick={props.back}>Back to brands</button>
        <a className="btn soft" href={`https://${props.domain}`} target="_blank" rel="noreferrer">Open brand URL</a>
      </Hero>
      <main>
        {message ? <div className={`notice ${message.includes("sent") || message.includes("cleared") ? "ok" : "error"}`}>{message}</div> : null}
        {props.crm?.synced_at ? <div className="notice ok">Brand CRM record synced {fmtDate(props.crm.synced_at, true)}.</div> : null}
        {props.crm?.sync_error ? <div className="notice error">Last CRM error: {props.crm.sync_error}</div> : null}
        <section className="card">
          <div className="card-head"><h2>Brand filters</h2></div>
          <div className="controls">
            <div><div className="label">Mention type</div><select className="field" value={props.filters.mentionType} onChange={(event) => props.setFilters({ ...props.filters, mentionType: event.target.value as BrandFilters["mentionType"] })}><option value="sponsored">Sponsored only</option><option value="affiliate">Affiliate only</option><option value="link">Links only</option><option value="all">All types</option></select></div>
          </div>
        </section>
        <section className="grid">
          <Stat label="Matched creators" value={fmtInt(creators.length)} subvalue="unique creators" />
          <Stat label="Matched videos" value={fmtInt(videos.length)} subvalue="videos with this domain" />
          <Stat label="Raw links" value={fmtInt(classified.length)} subvalue="matching URLs" />
        </section>
        <TableCard title="Raw link matches" subtitle="Exact outbound URLs and description text that caused this brand to be detected." columns={["Creator", "Type", "URL", "Matched text", "Detected"]}>
          {classified.map((link) => <tr key={link.id}><td>{link.creators?.display_name ?? link.channel_id}</td><td className="type-cell"><Badge tone={badgeTone("link", link.mention_type)}>{link.mention_type}</Badge></td><td><a href={link.source_url} target="_blank" rel="noreferrer">open link</a></td><td>{link.evidence_text ?? "-"}</td><td>{fmtDate(link.detected_at, true)}</td></tr>)}
        </TableCard>
        <TableCard title="Videos" subtitle="Videos whose descriptions produced this brand match." columns={["Video", "Creator", "Type", "Views", "Linked URL"]}>
          {videos.map((link) => <tr key={`${link.video_id}:${link.id}`}><td><a href={link.videos?.video_url ?? link.source_url} target="_blank" rel="noreferrer">{link.videos?.title ?? link.video_id}</a></td><td>{link.creators?.display_name ?? link.channel_id}</td><td className="type-cell"><Badge tone={badgeTone("link", link.mention_type)}>{link.mention_type}</Badge></td><td>{fmtInt(link.videos?.view_count)}</td><td><a href={link.source_url} target="_blank" rel="noreferrer">open linked URL</a></td></tr>)}
        </TableCard>
        <TableCard title="Creators" subtitle="Creators connected to this brand domain." columns={["Creator", "Subscribers", "Tier"]}>
          {creators.map((link) => <tr key={link.channel_id}><td><button className="link-button" onClick={() => link.channel_id && props.openCreator(link.channel_id)}><strong>{link.creators?.display_name ?? link.channel_id}</strong></button></td><td>{fmtInt(link.creators?.subscriber_count)}</td><td>{link.creators?.tier ?? "-"}</td></tr>)}
        </TableCard>
      </main>
    </>
  );
}

function OpsPage(props: {
  rows: CreatorNiche[];
  opsSnapshot: OpsSnapshot;
  niches: string[];
  filters: Filters;
  runs: PipelineRun[];
  quotaLogs: QuotaLog[];
  searchKeywords: SearchKeyword[];
  runnerStatus: RunnerStatus | null;
  runnerBusy: boolean;
  snapshotLoading: boolean;
  loading: boolean;
  setFilters: (filters: Filters) => void;
  startRunnerStage: (stage: string, config: string, options?: { recent_videos_to_sample?: number }) => void;
  stopRunner: () => void;
  checkRunner: () => void;
  refresh: () => void;
  back: () => void;
}) {
  const runnerConfigs = props.runnerStatus?.configs ?? [];
  const [configContent, setConfigContent] = useState("");
  const [configMessage, setConfigMessage] = useState("");
  const [configLoading, setConfigLoading] = useState(false);
  const [showNewConfig, setShowNewConfig] = useState(false);
  const [newNicheName, setNewNicheName] = useState("");
  const [newSeedKeywords, setNewSeedKeywords] = useState('"travel vlog"\n"budget travel vlog"\n"travel guide"\n"things to do in" Tokyo');
  const [newNegativeFilter, setNewNegativeFilter] = useState("-female -woman -women -girl -couple -couples -husband -wife");
  const [newRegionCode, setNewRegionCode] = useState("US");
  const [newMinSubscribers, setNewMinSubscribers] = useState("10000");
  const [newMaxSubscribers, setNewMaxSubscribers] = useState("2000000");
  const [newMaxDaysSinceUpload, setNewMaxDaysSinceUpload] = useState("90");
  const [newMinEngagement, setNewMinEngagement] = useState("0.005");
  const [newRecentVideos, setNewRecentVideos] = useState("15");
  const [newCooldownDays, setNewCooldownDays] = useState("14");
  const nicheOptions = Array.from(new Set([...props.niches, ...runnerConfigs.map((config) => config.name)])).sort();
  const selectedNiche = props.filters.niche || nicheOptions[0] || "";
  const nicheSelectValue = showNewConfig ? ADD_NICHE_VALUE : selectedNiche;
  const scopedKeywords = selectedNiche ? props.searchKeywords.filter((row) => row.niche_config === selectedNiche) : props.searchKeywords;
  const scopedRuns = selectedNiche ? props.runs.filter((row) => row.niche_config === selectedNiche) : props.runs;
  const today = new Date().toISOString().slice(0, 10);
  const quotaToday = props.quotaLogs.filter((row) => row.run_date === today);
  const quotaByKey = Array.from(quotaToday.reduce((map, row) => {
    map.set(row.api_key_label, (map.get(row.api_key_label) ?? 0) + Number(row.units_used ?? 0));
    return map;
  }, new Map<string, number>()).entries()).sort(([a], [b]) => a.localeCompare(b));
  const quotaTotal = quotaByKey.reduce((sum, [, used]) => sum + used, 0);
  const summary = props.opsSnapshot;
  const stageCommands = [
    ["Full pipeline", "all", "Discovery, basic filter, then enrichment."],
    ["Discovery", "discovery", "Search YouTube seed keywords and queue new channels."],
    ["Stage 1", "stage1", "Apply channel basics and subscriber filters."],
    ["Stage 2", "stage2", "Fetch videos, score engagement, extract links."],
  ];
  const selectedConfig = selectedNiche
    ? runnerConfigs.find((config) => config.name === selectedNiche)?.path ?? `config/niche_${selectedNiche}.yaml`
    : "";
  const runnerConnected = Boolean(props.runnerStatus?.ok);
  const runnerJob = props.runnerStatus?.current ?? props.runnerStatus?.last ?? null;
  const canRunSelectedNiche = runnerConnected && Boolean(selectedConfig) && !showNewConfig;
  const hasOpsSnapshot = Number(summary.total ?? 0) > 0;
  const opsStatValue = (value: number) => props.snapshotLoading && !hasOpsSnapshot ? "-" : fmtInt(value);
  const opsStatSubvalue = (value: string) => props.snapshotLoading ? "updating..." : value;
  const configMessageOk = configMessage === "Config saved." || configMessage === "Config created.";

  useEffect(() => {
    if (!runnerConnected || !selectedConfig || showNewConfig) {
      setConfigContent("");
      return;
    }
    setConfigLoading(true);
    setConfigMessage("");
    fetch(`${LOCAL_RUNNER_URL}/config?${new URLSearchParams({ path: selectedConfig }).toString()}`)
      .then((response) => response.json())
      .then((data) => {
        if (!data.ok) throw new Error(data.error ?? "Could not load config");
        setConfigContent(data.content ?? "");
      })
      .catch((error) => setConfigMessage(error instanceof Error ? error.message : "Could not load config"))
      .finally(() => setConfigLoading(false));
  }, [runnerConnected, selectedConfig, showNewConfig]);

  async function saveConfig() {
    setConfigLoading(true);
    setConfigMessage("");
    try {
      const response = await fetch(`${LOCAL_RUNNER_URL}/config`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ path: selectedConfig, content: configContent }),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error ?? "Could not save config");
      setConfigMessage("Config saved.");
      props.checkRunner();
    } catch (error) {
      setConfigMessage(error instanceof Error ? error.message : "Could not save config");
    } finally {
      setConfigLoading(false);
    }
  }

  async function createConfig() {
    const safeName = newNicheName.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
    if (!safeName) {
      setConfigMessage("Enter a niche name.");
      return;
    }
    const keywords = newSeedKeywords
      .split(/\r?\n/)
      .map((keyword) => keyword.trim())
      .filter(Boolean);
    if (!keywords.length) {
      setConfigMessage("Add at least one seed keyword.");
      return;
    }
    const path = `config/niche_${safeName}.yaml`;
    const keywordYaml = keywords.map((keyword) => `  - ${JSON.stringify(keyword)}`).join("\n");
    const negativeFilter = newNegativeFilter.trim();
    const content = `niche_name: "${safeName}"\n\nseed_keywords:\n${keywordYaml}\n${negativeFilter ? `\nglobal_negative_filter: ${JSON.stringify(negativeFilter)}\n` : ""}\ndiscovery_orders:\n  - "relevance"\n\nmin_subscribers: ${Number(newMinSubscribers) || 10000}\nmax_subscribers: ${Number(newMaxSubscribers) || 2000000}\nmax_days_since_upload: ${Number(newMaxDaysSinceUpload) || 90}\nmin_engagement_per_sub: ${Number(newMinEngagement) || 0.005}\nregion_code: "${newRegionCode.trim().toUpperCase() || "US"}"\nkeyword_cooldown_days: ${Number(newCooldownDays) || 14}\ndaily_quota_budget_per_key: 10000\nrecent_videos_to_sample: ${Number(newRecentVideos) || 15}\nshort_max_duration_seconds: 180\nmin_long_videos_for_scoring: 5\n`;
    setConfigLoading(true);
    try {
      const response = await fetch(`${LOCAL_RUNNER_URL}/config`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ path, content }),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error ?? "Could not create config");
      setNewNicheName("");
      setNewSeedKeywords('"travel vlog"\n"budget travel vlog"\n"travel guide"\n"things to do in" Tokyo');
      setConfigContent(content);
      setShowNewConfig(false);
      props.setFilters({ ...props.filters, niche: safeName });
      props.checkRunner();
      setConfigMessage("Config created.");
    } catch (error) {
      setConfigMessage(error instanceof Error ? error.message : "Could not create config");
    } finally {
      setConfigLoading(false);
    }
  }

  return (
    <>
      <Hero title="Pipeline ops" copy="Watch shared Supabase pipeline state, quota, run history, and keyword cooldowns across every device.">
        <button className="btn secondary" onClick={props.back}>Back to board</button>
        <button className="btn soft" onClick={props.refresh}><RefreshCw size={15} />Refresh ops</button>
      </Hero>
      <main>
        <section className="card ops-overview">
          <div className="ops-overview-head">
            <div className="card-head"><h2>Current board snapshot</h2><p>Counts from Supabase creator_niches for the selected niche.</p></div>
            <div className="config-strip">
              <div><div className="label">Niche</div><select className="field" value={nicheSelectValue} onChange={(event) => {
                if (event.target.value === ADD_NICHE_VALUE) {
                  setShowNewConfig(true);
                  setConfigMessage("");
                  return;
                }
                setShowNewConfig(false);
                props.setFilters({ ...props.filters, niche: event.target.value });
              }}>{nicheOptions.map((niche) => <option key={niche} value={niche}>{niche}</option>)}<option value={ADD_NICHE_VALUE}>+ Add niche config</option></select></div>
            </div>
          </div>
          <div className="grid">
            <Stat label="Creators" value={opsStatValue(summary.total)} subvalue={opsStatSubvalue("in this scope")} tone="strong" />
            <Stat label="Discovered" value={opsStatValue(summary.discovered)} subvalue={opsStatSubvalue("waiting for stage 1")} />
            <Stat label="Basic checked" value={opsStatValue(summary.stage1)} subvalue={opsStatSubvalue("ready for enrichment")} tone="ok" />
            <Stat label="Vetted" value={opsStatValue(summary.vetted)} subvalue={opsStatSubvalue("available on board")} tone="strong" />
            <Stat label="Rejected" value={opsStatValue(summary.disqualified)} subvalue={opsStatSubvalue("filtered out")} tone="weak" />
            <Stat label="Keywords" value={props.snapshotLoading && !scopedKeywords.length ? "-" : fmtInt(scopedKeywords.length)} subvalue={props.snapshotLoading ? "updating..." : "seed cooldown rows"} />
          </div>
        </section>
        <section className="ops-layout">
          <div className="ops-main">
            <section className="card">
              <div className="card-head local-runner-head">
                <div className="title-row"><h2>Local runner</h2><Badge tone={runnerConnected ? "strong" : "weak"}>{runnerConnected ? "connected" : "offline"}</Badge></div>
                <p>Start pipeline stages from this machine. Supabase still stores the shared run history and dedupe state.</p>
              </div>
              {props.runnerStatus?.running ? (
                <div className="stack runner-controls">
                  <button className="btn secondary" disabled={props.runnerBusy} onClick={props.stopRunner}>Stop current run</button>
                </div>
              ) : null}
              {!runnerConnected ? (
                <div className="notice error">
                  Local runner is not connected. Start it with <code>python scripts/local_runner.py</code>, then refresh this page.
                </div>
              ) : null}
              <div className="stage-grid">
                {stageCommands.map(([title, stage, copy]) => (
                  <div className="stage-card" key={stage}>
                    <div><strong>{title}</strong><p>{copy}</p></div>
                    <button
                      className="btn soft"
                      disabled={!canRunSelectedNiche || props.runnerBusy || props.runnerStatus?.running}
                      onClick={() => props.startRunnerStage(stage, selectedConfig)}
                    >
                      Run {stage}
                    </button>
                  </div>
                ))}
              </div>
              {props.runnerStatus?.error ? <div className="notice error">{props.runnerStatus.error}</div> : null}
              {runnerJob ? (
                <div className="runner-output">
                  <div className="topline">
                    <strong>{runnerJob.stage} / {runnerJob.status}</strong>
                    <span className="muted small">{runnerJob.started_at}{runnerJob.ended_at ? ` -> ${runnerJob.ended_at}` : ""}</span>
                  </div>
                  <pre>{runnerJob.output || "No output yet."}</pre>
                </div>
              ) : null}
            </section>
            <TableCard title="Recent runs" subtitle={props.loading ? "Refreshing run history..." : `${scopedRuns.length} recent runs in this scope`} columns={["Status", "Stage", "Niche", "Device", "Started", "Ended"]}>
              {scopedRuns.map((run) => <tr key={run.id}><td><span className={`run-dot ${run.status ?? ""}`} />{label(run.status)}</td><td>{run.stage ?? "-"}</td><td>{run.niche_config ?? "-"}</td><td>{run.source_device ?? "-"}</td><td>{fmtDate(run.started_at, true)}</td><td>{fmtDate(run.ended_at, true)}</td></tr>)}
              {!scopedRuns.length ? <tr><td colSpan={6} className="muted">No pipeline runs recorded yet.</td></tr> : null}
            </TableCard>
            <TableCard title="Keyword cooldowns" subtitle={`${scopedKeywords.length} seed keywords`} columns={["Keyword", "Niche", "Last used", "Created"]}>
              {scopedKeywords.slice(0, 250).map((keyword) => <tr key={`${keyword.niche_config}:${keyword.keyword}`}><td><strong>{keyword.keyword}</strong></td><td>{keyword.niche_config}</td><td>{keyword.last_used_at ? `${fmtRelative(keyword.last_used_at)} (${fmtDate(keyword.last_used_at, true)})` : "never"}</td><td>{fmtDate(keyword.created_at, true)}</td></tr>)}
              {!scopedKeywords.length ? <tr><td colSpan={4} className="muted">No seed keywords loaded yet.</td></tr> : null}
            </TableCard>
          </div>
          <div className="ops-side">
            <Collapsible title="YAML config" subtitle="Edit local config files through the local runner. Works from Cloudflare only while this machine runner is open." defaultOpen={false}>
              {!runnerConnected ? (
                <div className="notice error">Start <code>python scripts/local_runner.py</code> to edit YAML configs from the dashboard.</div>
              ) : showNewConfig ? (
                <>
                  <div className="card-head"><h2>New niche config</h2><p>Create a YAML config with seed keywords, negative filters, and scoring thresholds.</p></div>
                  <div className="config-form">
                    <div className="controls two">
                      <div><div className="label">Niche name</div><input className="field" value={newNicheName} onChange={(event) => setNewNicheName(event.target.value)} placeholder="travel_europe" /></div>
                      <div><div className="label">Region</div><input className="field" value={newRegionCode} onChange={(event) => setNewRegionCode(event.target.value)} placeholder="US" /></div>
                    </div>
                    <div className="mt-4"><div className="label">Seed keywords, one per line</div><textarea className="field keyword-editor" value={newSeedKeywords} onChange={(event) => setNewSeedKeywords(event.target.value)} /></div>
                    <div className="mt-4"><div className="label">Global negative filter</div><input className="field" value={newNegativeFilter} onChange={(event) => setNewNegativeFilter(event.target.value)} placeholder="-female -woman -couple" /></div>
                    <div className="controls two mt-4">
                      <div><div className="label">Min subscribers</div><input className="field" inputMode="numeric" value={newMinSubscribers} onChange={(event) => setNewMinSubscribers(event.target.value)} /></div>
                      <div><div className="label">Max subscribers</div><input className="field" inputMode="numeric" value={newMaxSubscribers} onChange={(event) => setNewMaxSubscribers(event.target.value)} /></div>
                      <div><div className="label">Max days since upload</div><input className="field" inputMode="numeric" value={newMaxDaysSinceUpload} onChange={(event) => setNewMaxDaysSinceUpload(event.target.value)} /></div>
                      <div><div className="label">Min engagement / sub</div><input className="field" inputMode="decimal" value={newMinEngagement} onChange={(event) => setNewMinEngagement(event.target.value)} /></div>
                      <div><div className="label">Videos to sample</div><input className="field" inputMode="numeric" value={newRecentVideos} onChange={(event) => setNewRecentVideos(event.target.value)} /></div>
                      <div><div className="label">Cooldown days</div><input className="field" inputMode="numeric" value={newCooldownDays} onChange={(event) => setNewCooldownDays(event.target.value)} /></div>
                    </div>
                    <button className="btn secondary mt-4" disabled={configLoading} onClick={createConfig}>Create config</button>
                  </div>
                  {configMessage ? <div className={`notice ${configMessageOk ? "ok" : "error"}`}>{configMessage}</div> : null}
                </>
              ) : (
                <>
                  <div className="topline">
                    <div><div className="label">Editing</div><code className="code-inline">{selectedConfig}</code></div>
                    <button className="btn soft" disabled={configLoading || !selectedConfig} onClick={saveConfig}>Save YAML</button>
                  </div>
                  <textarea className="field yaml-editor" value={configContent} onChange={(event) => setConfigContent(event.target.value)} spellCheck={false} />
                  {configMessage ? <div className={`notice ${configMessageOk ? "ok" : "error"}`}>{configMessage}</div> : null}
                </>
              )}
            </Collapsible>
            <Collapsible title="Quota today" subtitle={`${fmtInt(quotaTotal)} total units used today across ${quotaByKey.length} key(s).`} defaultOpen={false}>
              {quotaByKey.map(([key, used]) => <div className="panel-item" key={key}><div className="topline"><strong>{key}</strong><Badge tone="sky">{fmtInt(used)} units</Badge></div></div>)}
              {!quotaByKey.length ? <div className="muted">No quota usage logged today.</div> : null}
            </Collapsible>
          </div>
        </section>
      </main>
    </>
  );
}

createRoot(document.getElementById("root")!).render(<App />);
