import { useEffect, useMemo, useRef, useState } from "react";
import { geoAlbersUsa, geoPath } from "d3-geo";
import { feature } from "topojson-client";
import statesAtlas from "us-atlas/states-10m.json";

const MAP_WIDTH = 1000;
const MAP_HEIGHT = 620;
const US_STATE_FEATURES = feature(statesAtlas, statesAtlas.objects.states);

const ALL_US_STATES = [
  ["AL", "Alabama"], ["AK", "Alaska"], ["AZ", "Arizona"], ["AR", "Arkansas"],
  ["CA", "California"], ["CO", "Colorado"], ["CT", "Connecticut"], ["DE", "Delaware"],
  ["DC", "District of Columbia"], ["FL", "Florida"], ["GA", "Georgia"], ["HI", "Hawaii"],
  ["ID", "Idaho"], ["IL", "Illinois"], ["IN", "Indiana"], ["IA", "Iowa"],
  ["KS", "Kansas"], ["KY", "Kentucky"], ["LA", "Louisiana"], ["ME", "Maine"],
  ["MD", "Maryland"], ["MA", "Massachusetts"], ["MI", "Michigan"], ["MN", "Minnesota"],
  ["MS", "Mississippi"], ["MO", "Missouri"], ["MT", "Montana"], ["NE", "Nebraska"],
  ["NV", "Nevada"], ["NH", "New Hampshire"], ["NJ", "New Jersey"], ["NM", "New Mexico"],
  ["NY", "New York"], ["NC", "North Carolina"], ["ND", "North Dakota"], ["OH", "Ohio"],
  ["OK", "Oklahoma"], ["OR", "Oregon"], ["PA", "Pennsylvania"], ["PR", "Puerto Rico"],
  ["RI", "Rhode Island"], ["SC", "South Carolina"], ["SD", "South Dakota"], ["TN", "Tennessee"],
  ["TX", "Texas"], ["UT", "Utah"], ["VT", "Vermont"], ["VA", "Virginia"],
  ["WA", "Washington"], ["WV", "West Virginia"], ["WI", "Wisconsin"], ["WY", "Wyoming"],
].map(([code, name]) => ({ code, name }));

function fmt(n) { return Number(n || 0).toLocaleString(); }

function GeocodeMap({ result }) {
  const projection = useMemo(
    () => geoAlbersUsa().fitExtent([[24, 18], [MAP_WIDTH - 24, MAP_HEIGHT - 18]], US_STATE_FEATURES),
    [],
  );
  const mapPath = useMemo(() => geoPath(projection), [projection]);
  const hasCoords = result?.found && Number.isFinite(result.latitude) && Number.isFinite(result.longitude);
  const projected = hasCoords ? projection([result.longitude, result.latitude]) : null;
  const label = result?.displayName || (result?.city && result?.state ? `${result.city}, ${result.state}` : "");

  return (
    <div className="rounded-box bg-base-100 p-3">
      <div className="mb-2 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
        <p className="text-xs text-base-content/50">Map</p>
        {hasCoords && (
          <span className="badge badge-primary badge-sm font-mono">
            {result.latitude.toFixed(4)}, {result.longitude.toFixed(4)}
          </span>
        )}
      </div>
      <div className="relative overflow-hidden rounded-box border border-base-300 bg-base-200/40">
        <svg
          className="h-auto w-full"
          viewBox={`0 0 ${MAP_WIDTH} ${MAP_HEIGHT}`}
          role="img"
          aria-label={hasCoords ? `Map pin for ${label}` : "US map without a pin"}
        >
          {US_STATE_FEATURES.features.map((loc) => (
            <path key={loc.id} d={mapPath(loc)} className="fill-base-300 stroke-base-100" strokeWidth="1.2" opacity="0.72" />
          ))}
          {projected && (
            <g transform={`translate(${projected[0]} ${projected[1]})`}>
              <circle r="18" className="fill-primary" opacity="0.16" />
              <circle r="8" className="fill-primary stroke-base-100" strokeWidth="3" />
              <path
                d="M 0 -29 C -10 -29 -18 -21 -18 -11 C -18 3 0 24 0 24 C 0 24 18 3 18 -11 C 18 -21 10 -29 0 -29 Z"
                className="fill-error stroke-base-100"
                strokeWidth="3"
              />
              <circle cy="-11" r="5" className="fill-base-100" />
            </g>
          )}
        </svg>
        {hasCoords && (
          <div className="absolute bottom-3 left-3 max-w-[calc(100%-1.5rem)] rounded-field border border-base-300 bg-base-100/95 px-3 py-2 text-sm shadow-sm">
            <p className="truncate font-semibold">{label}</p>
            <p className="font-mono text-xs text-base-content/60">{result.latitude.toFixed(6)}, {result.longitude.toFixed(6)}</p>
          </div>
        )}
      </div>
    </div>
  );
}

function StateCombobox({ value, onChange, options, placeholder = "State" }) {
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const [highlight, setHighlight] = useState(0);
  const wrapRef = useRef(null);
  const inputRef = useRef(null);
  const selected = options.find((s) => s.code === value || s.name.toUpperCase() === String(value || "").toUpperCase());
  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return options;
    return options.filter((s) => s.code.toLowerCase().startsWith(q) || s.name.toLowerCase().includes(q));
  }, [options, query]);
  useEffect(() => {
    function handleClick(event) {
      if (wrapRef.current && !wrapRef.current.contains(event.target)) {
        setOpen(false);
        setQuery("");
      }
    }
    document.addEventListener("mousedown", handleClick);
    return () => document.removeEventListener("mousedown", handleClick);
  }, []);
  useEffect(() => { setHighlight(0); }, [query, open]);
  const display = open ? query : (selected ? `${selected.code} — ${selected.name}` : (value || ""));
  function pick(s) {
    onChange(s.code);
    setOpen(false);
    setQuery("");
    inputRef.current?.blur();
  }
  return (
    <div ref={wrapRef} className="relative">
      <input
        ref={inputRef}
        type="text"
        className="input input-bordered w-full"
        value={display}
        placeholder={placeholder}
        autoComplete="off"
        onFocus={() => { setOpen(true); setQuery(""); }}
        onChange={(event) => { setQuery(event.target.value); setOpen(true); }}
        onKeyDown={(event) => {
          if (event.key === "Escape") { setOpen(false); setQuery(""); inputRef.current?.blur(); }
          else if (event.key === "ArrowDown") { event.preventDefault(); setOpen(true); setHighlight((h) => Math.min(h + 1, filtered.length - 1)); }
          else if (event.key === "ArrowUp") { event.preventDefault(); setHighlight((h) => Math.max(h - 1, 0)); }
          else if (event.key === "Enter") {
            event.preventDefault();
            if (filtered[highlight]) pick(filtered[highlight]);
          }
        }}
      />
      {open && (
        <ul className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-box border border-base-300 bg-base-100 shadow-lg">
          {filtered.length === 0 && <li className="px-3 py-2 text-sm text-base-content/50">No match</li>}
          {filtered.map((s, idx) => (
            <li key={s.code}>
              <button
                type="button"
                className={"flex w-full items-baseline gap-2 px-3 py-1.5 text-left text-sm hover:bg-base-200 " + (idx === highlight ? "bg-base-200" : "")}
                onMouseEnter={() => setHighlight(idx)}
                onMouseDown={(event) => { event.preventDefault(); pick(s); }}
              >
                <span className="badge badge-ghost badge-xs font-mono">{s.code}</span>
                <span>{s.name}</span>
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

function CopyButton({ getText, label = "Copy" }) {
  const [copied, setCopied] = useState(false);
  return (
    <button
      type="button"
      title={copied ? "Copied" : label}
      aria-label={copied ? "Copied" : label}
      className="btn btn-ghost btn-xs"
      onClick={async (event) => {
        event.preventDefault();
        event.stopPropagation();
        try {
          await navigator.clipboard.writeText(getText());
          setCopied(true);
          setTimeout(() => setCopied(false), 1500);
        } catch {
          // Clipboard unavailable (e.g., insecure context). Silently no-op.
        }
      }}
    >
      {copied ? (
        <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="20 6 9 17 4 12" />
        </svg>
      ) : (
        <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="9" y="9" width="13" height="13" rx="2" />
          <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
        </svg>
      )}
    </button>
  );
}

const PROVIDER_PREF_KEY = "geocoder.aiProvider";
const THEME_PREF_KEY = "geocoder.theme";

// Persisted manual choice if present, otherwise the OS preference.
function initialTheme() {
  try {
    const saved = localStorage.getItem(THEME_PREF_KEY);
    if (saved === "light" || saved === "dark") return saved;
  } catch {}
  try {
    if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark";
  } catch {}
  return "light";
}

function SunIcon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
      <circle cx="12" cy="12" r="4" />
      <path strokeLinecap="round" d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41" />
    </svg>
  );
}

function MoonIcon() {
  return (
    <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
      <path strokeLinecap="round" strokeLinejoin="round" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
    </svg>
  );
}

export default function App() {
  const [city, setCity] = useState("Austin");
  const [state, setState] = useState("TX");
  const [useAi, setUseAi] = useState(false);
  const [status, setStatus] = useState({ kind: "idle", message: "" });
  const [result, setResult] = useState(null);
  const [health, setHealth] = useState(null);
  const [providerPref, setProviderPref] = useState(() => {
    try { return localStorage.getItem(PROVIDER_PREF_KEY); } catch { return null; }
  });
  const [theme, setTheme] = useState(initialTheme);

  // Reflect the active theme onto <html data-theme> for daisyUI to pick up.
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
  }, [theme]);

  function toggleTheme() {
    setTheme((prev) => {
      const next = prev === "dark" ? "light" : "dark";
      try { localStorage.setItem(THEME_PREF_KEY, next); } catch {}
      return next;
    });
  }

  useEffect(() => {
    fetch("/api/health").then((r) => r.json()).then(setHealth).catch(() => setHealth(null));
  }, []);

  // If the persisted preference is no longer in the configured list (key
  // removed from .env), clear it so the server's default takes over.
  useEffect(() => {
    if (!providerPref || !health?.ai?.configured) return;
    const stillAvailable = health.ai.configured.some((p) => p.name === providerPref);
    if (!stillAvailable) {
      setProviderPref(null);
      try { localStorage.removeItem(PROVIDER_PREF_KEY); } catch {}
    }
  }, [health, providerPref]);

  function chooseProvider(name) {
    setProviderPref(name);
    try { localStorage.setItem(PROVIDER_PREF_KEY, name); } catch {}
  }

  const configured = health?.ai?.configured || [];
  const activeProvider = providerPref || health?.ai?.provider || null;
  const activeProviderInfo = configured.find((p) => p.name === activeProvider) || null;

  async function runTest(event) {
    event?.preventDefault?.();
    const c = city.trim();
    const s = state.trim();
    if (!c || !s) {
      setStatus({ kind: "error", message: "Enter a city and state." });
      return;
    }
    setStatus({ kind: "loading", message: useAi ? "Looking up (AI fallback enabled)…" : "Looking up…" });
    setResult(null);
    try {
      const params = new URLSearchParams({ city: c, state: s });
      if (useAi) {
        params.set("useAi", "true");
        if (providerPref) params.set("provider", providerPref);
      }
      const response = await fetch("/api/geocode?" + params.toString());
      const payload = await response.json().catch(() => ({}));
      if (!response.ok) throw new Error(payload.error || "Geocode failed with " + response.status);
      setResult(payload);
      const label = payload.displayName || `${c}, ${s}`;
      const via = payload.matchMethod === "ai" ? " via AI" : "";
      setStatus({
        kind: payload.found ? "ready" : "empty",
        message: payload.found
          ? `Matched ${label}${via} in ${fmt(payload.elapsedMs)} ms.`
          : `No exact match for ${c}, ${s}.`,
      });
    } catch (err) {
      setStatus({ kind: "error", message: err.message || "Lookup failed." });
    }
  }

  const aiDisabled = health && !health.ai?.available;

  return (
    <div className="min-h-screen bg-base-200 py-8 px-4">
      <div className="relative mx-auto max-w-4xl space-y-6">
        <button
          type="button"
          onClick={toggleTheme}
          className="btn btn-ghost btn-circle btn-sm absolute right-0 top-0 z-50"
          aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
          title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
        >
          {theme === "dark" ? <SunIcon /> : <MoonIcon />}
        </button>
        <header className="flex flex-wrap items-end justify-between gap-3">
          <div>
            <h1 className="text-2xl font-bold">US Census Geocoder</h1>
            <p className="text-sm text-base-content/60">Offline city/state → lat/lng lookup against the US Census 2024 Gazetteer.</p>
          </div>
          <div className="flex flex-wrap items-center gap-2 text-xs">
            <span className="badge badge-outline">US Census 2024</span>
            <span className="badge badge-outline">SQLite</span>
            {health?.ai?.available
              ? <span className="badge badge-success">AI: {activeProviderInfo?.name || health.ai.provider} · {activeProviderInfo?.model || health.ai.model}</span>
              : <span className="badge badge-ghost" title={health?.ai?.reason || ""}>AI: disabled</span>}
          </div>
        </header>

        <section className="card border border-base-300 bg-base-100 shadow-sm">
          <form className="card-body gap-4 p-4" onSubmit={runTest}>
            <div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_14rem_auto] md:items-end">
              <label className="form-control">
                <span className="label-text">City</span>
                <input
                  className="input input-bordered w-full"
                  value={city}
                  onChange={(event) => setCity(event.target.value)}
                  placeholder="Austin"
                  autoComplete="address-level2"
                />
              </label>
              <label className="form-control">
                <span className="label-text">State</span>
                <StateCombobox
                  value={state}
                  onChange={setState}
                  options={ALL_US_STATES}
                  placeholder="Search states"
                />
              </label>
              <button className="btn btn-primary" type="submit" disabled={status.kind === "loading"}>
                {status.kind === "loading" ? "Looking up…" : "Geocode"}
              </button>
            </div>

            <div className="flex flex-col gap-2 self-start">
              <label className={"label cursor-pointer justify-start gap-3 " + (aiDisabled ? "opacity-50" : "")}>
                <input
                  type="checkbox"
                  className="toggle toggle-sm toggle-primary"
                  checked={useAi && !aiDisabled}
                  disabled={aiDisabled}
                  onChange={(event) => setUseAi(event.target.checked)}
                />
                <span className="label-text">
                  AI fallback
                  {health?.ai?.available && configured.length === 1 && (
                    <span className="ml-2 badge badge-outline badge-xs font-mono">{configured[0].name} · {configured[0].model}</span>
                  )}
                  <span className="ml-2 text-xs text-base-content/50">
                    {aiDisabled
                      ? (health?.ai?.reason || "no provider configured")
                      : "runs only when gazetteer misses"}
                  </span>
                </span>
              </label>

              {configured.length > 1 && (
                <div className={"flex flex-wrap items-center gap-2 pl-12 " + (useAi ? "" : "opacity-50")}>
                  <span className="text-xs text-base-content/50">Provider:</span>
                  {configured.map((p) => {
                    const isActive = (providerPref || configured[0].name) === p.name;
                    return (
                      <button
                        key={p.name}
                        type="button"
                        disabled={!useAi}
                        onClick={() => chooseProvider(p.name)}
                        title={`${p.name} · ${p.model}`}
                        className={
                          "badge badge-sm font-mono cursor-pointer transition " +
                          (isActive ? "badge-primary" : "badge-outline hover:badge-neutral")
                        }
                      >
                        {p.name} · {p.model}
                      </button>
                    );
                  })}
                </div>
              )}
            </div>

            {status.message && (
              <div className={"alert py-2 " + (status.kind === "error" ? "alert-error" : status.kind === "empty" ? "alert-warning" : status.kind === "ready" ? "alert-success" : "alert-info")}>
                <span>{status.message}</span>
              </div>
            )}

            {result && (
              <div className="space-y-3 rounded-box border border-base-300 bg-base-200/40 p-4">
                <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
                  <div className="rounded-box bg-base-100 p-3">
                    <p className="text-xs text-base-content/50">Latitude</p>
                    <p className="font-mono text-lg font-bold">{result.found ? Number(result.latitude).toFixed(6) : "—"}</p>
                  </div>
                  <div className="rounded-box bg-base-100 p-3">
                    <p className="text-xs text-base-content/50">Longitude</p>
                    <p className="font-mono text-lg font-bold">{result.found ? Number(result.longitude).toFixed(6) : "—"}</p>
                  </div>
                  <div className="rounded-box bg-base-100 p-3">
                    <p className="text-xs text-base-content/50">Elapsed</p>
                    <p className="text-lg font-bold">{fmt(result.elapsedMs)} ms</p>
                  </div>
                  <div className="rounded-box bg-base-100 p-3">
                    <p className="text-xs text-base-content/50">Method</p>
                    <p className="text-lg font-bold">{result.matchMethod || "—"}</p>
                  </div>
                </div>

                {result.displayName && (
                  <div className="rounded-box bg-base-100 p-3">
                    <p className="text-xs text-base-content/50">Match</p>
                    <p className="mt-1 text-sm font-medium">{result.displayName}</p>
                    <div className="mt-2 flex flex-wrap gap-2">
                      {result.match?.geoid && <span className="badge badge-ghost badge-sm">geoid: {result.match.geoid}</span>}
                      {result.match?.lsad && <span className="badge badge-ghost badge-sm">lsad: {result.match.lsad}</span>}
                      {result.match?.fullName && <span className="badge badge-ghost badge-sm">{result.match.fullName}</span>}
                    </div>
                  </div>
                )}

                <GeocodeMap result={result} />

                {result.suggestions?.length > 0 && (
                  <div className="rounded-box bg-base-100 p-3">
                    <p className="text-xs text-base-content/50">Suggestions</p>
                    <ul className="mt-2 space-y-1 text-sm">
                      {result.suggestions.map((s, idx) => (
                        <li key={`${s.geoid}-${idx}`} className="flex flex-wrap items-center gap-2">
                          <span className="font-medium">{s.displayName}</span>
                          {s.source === "ai"
                            ? <span className="badge badge-info badge-xs">ai · {s.confidence}</span>
                            : <span className="badge badge-ghost badge-xs">fuzzy · d={s.distance}</span>}
                          <span className="text-xs font-mono text-base-content/50">{Number(s.latitude).toFixed(4)}, {Number(s.longitude).toFixed(4)}</span>
                        </li>
                      ))}
                    </ul>
                  </div>
                )}

                {result.aiCorrection && !result.aiCorrection.error && (
                  <div className="rounded-box bg-base-100 p-3">
                    <p className="text-xs text-base-content/50">AI fallback</p>
                    <p className="mt-1 text-sm">
                      <span className="font-medium">{result.aiCorrection.correctedCityName || "no match"}</span>
                      <span className="ml-2 badge badge-ghost badge-xs">confidence: {result.aiCorrection.confidence}</span>
                      <span className="ml-2 text-xs text-base-content/50">{fmt(result.aiCorrection.elapsedMs)} ms · {fmt(result.aiCorrection.candidateCount)} candidates</span>
                    </p>
                    {result.aiCorrection.reasoning && (
                      <p className="mt-1 text-xs italic text-base-content/60">{result.aiCorrection.reasoning}</p>
                    )}
                    {result.aiCorrection.usage && (
                      <div className="mt-3 border-t border-base-300 pt-3">
                        <div className="flex flex-wrap items-baseline justify-between gap-2">
                          <span className="text-xs text-base-content/50">Tokens & cost</span>
                          <span className="badge badge-outline badge-sm font-mono">
                            {result.aiCorrection.provider ? `${result.aiCorrection.provider} · ` : ""}{result.aiCorrection.model}
                          </span>
                        </div>
                        <div className="mt-2 grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
                          <div className="rounded bg-base-200/60 px-2 py-1">
                            <p className="text-base-content/50">Input</p>
                            <p className="font-mono font-semibold">{fmt(result.aiCorrection.usage.input_tokens || 0)}</p>
                          </div>
                          <div className="rounded bg-base-200/60 px-2 py-1">
                            <p className="text-base-content/50">Output</p>
                            <p className="font-mono font-semibold">{fmt(result.aiCorrection.usage.output_tokens || 0)}</p>
                          </div>
                          <div className="rounded bg-base-200/60 px-2 py-1">
                            <p className="text-base-content/50">Cache write</p>
                            <p className="font-mono font-semibold">{fmt(result.aiCorrection.usage.cache_creation_input_tokens || 0)}</p>
                          </div>
                          <div className="rounded bg-base-200/60 px-2 py-1">
                            <p className="text-base-content/50">Cache read</p>
                            <p className="font-mono font-semibold">{fmt(result.aiCorrection.usage.cache_read_input_tokens || 0)}</p>
                          </div>
                        </div>
                        <div className="mt-2 flex items-baseline justify-between gap-2">
                          <span className="text-xs text-base-content/50">Cost (this call)</span>
                          <span className="font-mono text-sm font-bold">${(result.aiCorrection.costUsd ?? 0).toFixed(5)}</span>
                        </div>
                        {result.aiCorrection.pricingPerMTok && (
                          <p className="mt-1 text-[10px] text-base-content/40">
                            Rates / MTok: input ${result.aiCorrection.pricingPerMTok.input.toFixed(2)} · output ${result.aiCorrection.pricingPerMTok.output.toFixed(2)} · cache write ${result.aiCorrection.pricingPerMTok.cacheWrite.toFixed(2)} · cache read ${result.aiCorrection.pricingPerMTok.cacheRead.toFixed(2)}
                          </p>
                        )}
                      </div>
                    )}
                  </div>
                )}

                {result.aiCorrection?.error && (
                  <div className="rounded-box bg-warning/10 p-3">
                    <p className="text-xs font-medium text-warning">AI fallback: {result.aiCorrection.error}</p>
                  </div>
                )}

                <details className="collapse collapse-arrow rounded-box border border-base-300 bg-base-100">
                  <summary className="collapse-title flex items-center justify-between gap-3 pr-12 text-sm font-medium">
                    <span>Raw JSON</span>
                    <CopyButton getText={() => JSON.stringify(result, null, 2)} label="Copy raw JSON" />
                  </summary>
                  <div className="collapse-content">
                    <pre className="max-h-[24rem] overflow-auto rounded-box bg-neutral p-3 text-xs text-neutral-content"><code>{JSON.stringify(result, null, 2)}</code></pre>
                  </div>
                </details>
              </div>
            )}
          </form>
        </section>

        <footer className="pt-4 text-center text-xs text-base-content/40">
          Data: <a className="link" href="https://www.census.gov/geographies/reference-files/time-series/geo/gazetteer-files.html" target="_blank" rel="noreferrer">US Census 2024 Gazetteer (public domain)</a>.
          MIT licensed.
        </footer>
      </div>
    </div>
  );
}
