import { useCallback, useMemo, useState } from "react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import {
  CheckCircleIcon,
  ChevronDownIcon,
  XCircleIcon,
} from "@heroicons/react/16/solid";
import type { Model, Provider } from "@qltysh/fabro-api-client";
import { apiData, modelsApi } from "../lib/api-client";
import { useModels, useProviders } from "../lib/queries";
import { Spinner } from "../components/state";
import {
  Dot,
  Panel,
  PanelSkeleton,
  Row,
  SettingsPageIntro,
} from "../components/settings-panel";
import { plural } from "../lib/plural";
import {
  FilterButton,
  type FilterOption,
} from "../components/runs-list/filter-button";
import {
  SortHeader,
  type SortDirection,
} from "../components/runs-list/sort-header";
import { Tooltip } from "../components/ui";
import { formatContextWindow, formatTokensPerSecond } from "../lib/format";
import {
  modelOfferingKey,
  modelOfferingTestArgs,
} from "../lib/model-offerings";
import { useDebouncedValue } from "../hooks/effects";

export function meta() {
  return [{ title: "Models — Fabro" }];
}

export default function SettingsModels() {
  const query = useProviders();

  return (
    <div className="space-y-8">
      <SettingsPageIntro description="LLM providers configured on this Fabro server." />
      {query.data ? (
        <>
          <ProvidersPanel providers={query.data.data} />
          <ModelsSection providers={query.data.data} />
        </>
      ) : (
        <PanelSkeleton />
      )}
    </div>
  );
}

function ProvidersPanel({ providers }: { providers: Provider[] }) {
  const { configured, unconfigured, priorityProviderId } = useMemo(() => {
    const configured: Provider[] = [];
    const unconfigured: Provider[] = [];
    for (const provider of providers) {
      if (provider.configured) {
        configured.push(provider);
      } else {
        unconfigured.push(provider);
      }
    }
    const top = configured.reduce<Provider | null>(
      (best, p) => (!best || p.priority > best.priority ? p : best),
      null,
    );
    return { configured, unconfigured, priorityProviderId: top?.id ?? null };
  }, [providers]);
  const [showUnconfigured, setShowUnconfigured] = useState(false);
  const showUnconfiguredRows = configured.length === 0 || showUnconfigured;

  if (providers.length === 0) {
    return (
      <Panel title="Providers">
        <div className="px-4 py-6 text-sm text-fg-muted">
          No LLM providers in the catalog.
        </div>
      </Panel>
    );
  }

  return (
    <Panel title="Providers">
      {configured.map((provider) => (
        <ProviderRow
          key={provider.id}
          provider={provider}
          isPriority={provider.id === priorityProviderId}
        />
      ))}
      {showUnconfiguredRows
        ? unconfigured.map((provider) => (
            <ProviderRow key={provider.id} provider={provider} />
          ))
        : null}
      {configured.length > 0 && unconfigured.length > 0 ? (
        <button
          type="button"
          onClick={() => setShowUnconfigured((v) => !v)}
          aria-expanded={showUnconfigured}
          className="flex w-full items-center gap-1.5 px-4 py-3 text-left text-xs font-medium text-fg-muted hover:text-fg-3"
        >
          <ChevronDownIcon
            className={`size-4 h-lh shrink-0 transition-transform ${
              showUnconfigured ? "rotate-180" : ""
            }`}
          />
          {showUnconfigured ? "Hide" : "Show"} {unconfigured.length}{" "}
          unconfigured {plural(unconfigured.length, "provider", "providers")}
        </button>
      ) : null}
    </Panel>
  );
}

function ProviderRow({
  provider,
  isPriority = false,
}: {
  provider:    Provider;
  isPriority?: boolean;
}) {
  const name = provider.display_name || provider.id;
  const modelCount = `${provider.model_count} ${plural(provider.model_count, "model", "models")}`;
  return (
    <Row
      title={
        <span className="flex items-center gap-4">
          <ProviderLogo provider={provider} />
          <span className="flex min-w-0 flex-col">
            <span className="flex items-center gap-2 text-sm text-fg-2">
              {name}
              {isPriority ? <Label>Priority</Label> : null}
            </span>
            <span className="text-xs/5 text-fg-3">{modelCount}</span>
          </span>
        </span>
      }
    >
      <ProviderStatus provider={provider} />
    </Row>
  );
}

function Label({ children }: { children: ReactNode }) {
  return (
    <span className="inline-flex items-center rounded-md bg-overlay-strong px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-fg-3">
      {children}
    </span>
  );
}

function ProviderLogo({ provider }: { provider: Provider }) {
  const [failed, setFailed] = useState(false);
  const name = provider.display_name || provider.id;
  const chip =
    "grid size-10 shrink-0 place-items-center rounded-md bg-ice-50 ring-1 ring-line-strong";
  const dim = provider.configured ? "" : "opacity-60";

  if (failed) {
    const initial = name.charAt(0).toUpperCase() || "?";
    return (
      <span className={`${chip} text-base font-medium text-page ${dim}`}>
        {initial}
      </span>
    );
  }

  return (
    <span className={`${chip} ${dim}`}>
      <img
        alt=""
        src={`/images/providers/${provider.id}.svg`}
        onError={() => setFailed(true)}
        className="size-7"
      />
    </span>
  );
}

function ProviderStatus({ provider }: { provider: Provider }) {
  return (
    <span className="inline-flex flex-wrap items-center gap-x-2 gap-y-1">
      <span className="inline-flex items-center gap-2">
        <Dot on={provider.configured} />
        <span className={provider.configured ? "text-fg" : "text-fg-muted"}>
          {provider.configured ? "Configured" : "Not configured"}
        </span>
      </span>
      {!provider.configured && provider.api_key_url ? (
        <a
          href={provider.api_key_url}
          target="_blank"
          rel="noreferrer"
          className="text-xs text-teal-500 hover:underline"
        >
          Get API key →
        </a>
      ) : null}
      {!provider.configured && provider.expected_secret_name ? (
        <Link
          to={`/settings/secrets/new?name=${encodeURIComponent(provider.expected_secret_name)}`}
          className="text-xs text-teal-500 hover:underline"
        >
          Add secret →
        </Link>
      ) : null}
    </span>
  );
}

type ModelSortKey = "provider" | "model" | "context" | "speed";

type RowState =
  | { phase: "queued" }
  | { phase: "running" }
  | { phase: "ok" }
  | { phase: "error"; message: string };

type Sweep = {
  done:   number;
  total:  number;
  ok:     number;
  failed: number;
};

const TEST_CONCURRENCY = 4;

function ModelsSection({ providers }: { providers: Provider[] }) {
  const [providerFilter, setProviderFilter] = useState<string>("");
  const [searchInput, setSearchInput] = useState("");
  const debouncedSearch = useDebouncedValue(searchInput, 250);
  const [sortKey, setSortKey] = useState<ModelSortKey>("provider");
  const [direction, setDirection] = useState<SortDirection>("asc");
  const [results, setResults] = useState<Map<string, RowState>>(new Map());
  const [sweep, setSweep] = useState<Sweep | null>(null);

  const { data, isLoading } = useModels(providerFilter, debouncedSearch);

  const providerNameById = useMemo(() => {
    const map = new Map<string, string>();
    for (const p of providers) map.set(p.id, p.display_name || p.id);
    return map;
  }, [providers]);

  const rows = useMemo(() => {
    const all = (data?.data ?? []).filter((m) => m.configured);
    return sortModels(all, sortKey, direction, providerNameById);
  }, [data, sortKey, direction, providerNameById]);

  const providerOptions: FilterOption<string>[] = useMemo(() => {
    const options: FilterOption<string>[] = [{ value: "", label: "All providers" }];
    for (const provider of providers) {
      if (provider.configured) {
        options.push({ value: provider.id, label: provider.display_name || provider.id });
      }
    }
    return options;
  }, [providers]);

  const onSort = useCallback(
    (key: ModelSortKey) => {
      if (sortKey === key) {
        setDirection((dir) => (dir === "asc" ? "desc" : "asc"));
      } else {
        setSortKey(key);
        setDirection("asc");
      }
    },
    [sortKey],
  );

  const running = sweep !== null && sweep.done < sweep.total;

  const runSweep = useCallback(async () => {
    if (running) return;
    const offerings = rows.map((model) => ({
      id:       model.id,
      provider: model.provider,
      key:      modelOfferingKey(model),
    }));
    if (offerings.length === 0) return;

    const seed = new Map<string, RowState>();
    for (const offering of offerings) {
      seed.set(offering.key, { phase: "queued" });
    }
    setResults(seed);
    setSweep({ done: 0, total: offerings.length, ok: 0, failed: 0 });

    let cursor = 0;
    const worker = async () => {
      while (cursor < offerings.length) {
        const i = cursor;
        cursor += 1;
        const offering = offerings[i];
        setResults((prev) => {
          const next = new Map(prev);
          next.set(offering.key, { phase: "running" });
          return next;
        });

        let outcome: RowState;
        try {
          const result = await apiData(() =>
            modelsApi.testModel(...modelOfferingTestArgs(offering)),
          );
          if (
            result.provider !== offering.provider ||
            result.model_id !== offering.id
          ) {
            outcome = {
              phase:   "error",
              message: `Server tested unexpected offering ${result.provider}/${result.model_id}`,
            };
          } else if (result.status === "ok") {
            outcome = { phase: "ok" };
          } else if (result.status === "error") {
            outcome = {
              phase:   "error",
              message: result.error_message ?? "Failed",
            };
          } else {
            outcome = { phase: "error", message: "provider not configured" };
          }
        } catch (err) {
          outcome = {
            phase:   "error",
            message: err instanceof Error ? err.message : String(err),
          };
        }

        setResults((prev) => {
          const next = new Map(prev);
          next.set(offering.key, outcome);
          return next;
        });
        setSweep((prev) =>
          prev
            ? {
                done:   prev.done + 1,
                total:  prev.total,
                ok:     prev.ok + (outcome.phase === "ok" ? 1 : 0),
                failed: prev.failed + (outcome.phase === "error" ? 1 : 0),
              }
            : prev,
        );
      }
    };

    await Promise.all(
      Array.from({ length: Math.min(TEST_CONCURRENCY, offerings.length) }, () =>
        worker(),
      ),
    );
  }, [rows, running]);

  const showEmpty = !isLoading && rows.length === 0;

  return (
    <section className="space-y-3">
      <header className="flex flex-wrap items-center justify-between gap-3">
        <h2 className="text-sm font-medium text-fg-2">Models</h2>
        <div className="flex flex-wrap items-center gap-2">
          <FilterButton
            label="Provider"
            value={providerFilter}
            allValue=""
            options={providerOptions}
            onChange={setProviderFilter}
          />
          <input
            type="search"
            name="model-search"
            aria-label="Search models"
            placeholder="Search models…"
            value={searchInput}
            onChange={(e) => setSearchInput(e.target.value)}
            className="w-44 rounded-md border border-line bg-panel/80 px-3 py-2 text-xs text-fg-2 placeholder:text-fg-muted focus:border-line-strong focus:outline-none"
          />
          {sweep && !running ? (
            <span
              className={`text-xs tabular-nums ${sweep.failed === 0 ? "text-mint" : "text-coral"}`}
            >
              {sweep.ok} ok · {sweep.failed} failed
            </span>
          ) : null}
          <button
            type="button"
            onClick={() => void runSweep()}
            disabled={running || rows.length === 0}
            className="inline-flex items-center gap-1.5 rounded-md px-3 py-2 text-xs font-medium text-fg-2 ring-1 ring-line hover:bg-overlay/40 disabled:cursor-not-allowed disabled:opacity-60"
          >
            {running ? (
              <>
                <Spinner className="size-3.5 h-lh shrink-0 text-teal-500" />
                Testing… {sweep!.done}/{sweep!.total}
              </>
            ) : (
              "Test models"
            )}
          </button>
        </div>
      </header>
      <div className="-mx-4 -my-2 overflow-x-auto whitespace-nowrap sm:-mx-6 lg:-mx-8">
        <div className="inline-block min-w-full px-4 py-2 align-middle sm:px-6 lg:px-8">
          <table className="w-full text-sm [&_td:first-child]:pl-0 [&_td:last-child]:pr-0 [&_th:first-child]:pl-0 [&_th:last-child]:pr-0">
            <thead>
              <tr className="border-b border-line text-xs font-medium text-fg-3">
                <SortHeader<ModelSortKey>
                  label="Provider"
                  sortKey="provider"
                  activeSort={sortKey}
                  direction={direction}
                  onClick={onSort}
                />
                <SortHeader<ModelSortKey>
                  label="Model"
                  sortKey="model"
                  activeSort={sortKey}
                  direction={direction}
                  onClick={onSort}
                />
                <SortHeader<ModelSortKey>
                  label="Context"
                  sortKey="context"
                  activeSort={sortKey}
                  direction={direction}
                  align="right"
                  onClick={onSort}
                />
                <SortHeader<ModelSortKey>
                  label="Speed"
                  sortKey="speed"
                  activeSort={sortKey}
                  direction={direction}
                  align="right"
                  onClick={onSort}
                />
                <th
                  scope="col"
                  className="whitespace-nowrap px-3 py-2.5 text-right text-xs font-medium text-fg-3"
                >
                  Test
                </th>
              </tr>
            </thead>
            <tbody>
              {rows.map((model) => (
                <ModelTableRow
                  key={modelOfferingKey(model)}
                  model={model}
                  providerLabel={
                    providerNameById.get(model.provider) ?? model.provider
                  }
                  state={results.get(modelOfferingKey(model))}
                />
              ))}
            </tbody>
          </table>
        </div>
      </div>
      {showEmpty && (
        <div className="py-6 text-sm text-fg-muted">
          {debouncedSearch || providerFilter
            ? "No matching models from configured providers."
            : "No configured providers yet — add a provider above to enable models."}
        </div>
      )}
    </section>
  );
}

function ModelTableRow({
  model,
  providerLabel,
  state,
}: {
  model:         Model;
  providerLabel: string;
  state:         RowState | undefined;
}) {
  return (
    <tr className="border-b border-line last:border-b-0">
      <td className="whitespace-nowrap px-3 py-2.5 text-fg-3">
        {providerLabel}
      </td>
      <td className="whitespace-nowrap px-3 py-2.5">
        <ModelNameCell model={model} />
      </td>
      <td className="whitespace-nowrap px-3 py-2.5 text-right font-mono text-xs text-fg-muted tabular-nums">
        {formatContextWindow(model.limits.context_window)}
      </td>
      <td className="whitespace-nowrap px-3 py-2.5 text-right font-mono text-xs text-fg-muted tabular-nums">
        {formatTokensPerSecond(model.estimated_output_tps)}
      </td>
      <td className="whitespace-nowrap px-3 py-2.5 text-right">
        <TestStatusCell state={state} />
      </td>
    </tr>
  );
}

function TestStatusCell({ state }: { state: RowState | undefined }) {
  if (!state) {
    return <span className="text-xs text-fg-muted">—</span>;
  }
  if (state.phase === "queued") {
    return (
      <span className="inline-flex items-center" aria-label="Queued">
        <Spinner className="size-3.5 h-lh shrink-0 opacity-40" />
      </span>
    );
  }
  if (state.phase === "running") {
    return (
      <span className="inline-flex items-center text-fg-2" aria-label="Testing">
        <Spinner className="size-3.5 h-lh shrink-0 text-teal-500" />
      </span>
    );
  }
  if (state.phase === "ok") {
    return (
      <span className="inline-flex items-center text-mint" aria-label="Ok">
        <CheckCircleIcon className="size-4 h-lh shrink-0" />
      </span>
    );
  }
  return (
    <Tooltip
      label={
        <span className="block w-[32rem] max-w-[80vw] whitespace-pre-line break-words text-left font-mono leading-relaxed">
          {state.message}
        </span>
      }
    >
      <span
        aria-label={`Failed: ${state.message}`}
        className="inline-flex cursor-default items-center text-coral focus:outline-none"
        tabIndex={0}
      >
        <XCircleIcon className="size-4 h-lh shrink-0" />
      </span>
    </Tooltip>
  );
}

function ModelNameCell({ model }: { model: Model }) {
  const hasAliases = model.aliases.length > 0;
  const nameNode = hasAliases ? (
    <Tooltip
      label={
        <span className="block min-w-[10rem]">
          <span className="mb-1 block text-[10px] font-medium uppercase tracking-wider text-fg-muted">
            Aliases
          </span>
          {model.aliases.map((alias) => (
            <span key={alias} className="block font-mono text-fg-2">
              {alias}
            </span>
          ))}
        </span>
      }
    >
      <button
        type="button"
        className="cursor-default font-mono text-xs text-fg-2 underline decoration-dotted decoration-fg-muted underline-offset-4 hover:decoration-fg-3 focus:outline-none focus-visible:text-fg"
      >
        {model.id}
      </button>
    </Tooltip>
  ) : (
    <span className="font-mono text-xs text-fg-2">{model.id}</span>
  );

  if (!model.default && !model.small_default) {
    return nameNode;
  }

  return (
    <span className="inline-flex items-center gap-2">
      {nameNode}
      {model.default ? <Label>Default</Label> : null}
      {model.small_default ? <Label>Small</Label> : null}
    </span>
  );
}

function sortModels(
  models:           Model[],
  key:              ModelSortKey,
  direction:        SortDirection,
  providerNameById: Map<string, string>,
): Model[] {
  const sign = direction === "asc" ? 1 : -1;
  const providerLabel = (m: Model) =>
    providerNameById.get(m.provider) ?? m.provider;
  const sorted = Array.from(models);
  sorted.sort((a, b) => {
    let cmp = 0;
    switch (key) {
      case "provider":
        cmp = providerLabel(a).localeCompare(providerLabel(b));
        if (cmp === 0) cmp = a.id.localeCompare(b.id);
        break;
      case "model":
        cmp = a.id.localeCompare(b.id);
        break;
      case "context":
        cmp = a.limits.context_window - b.limits.context_window;
        if (cmp === 0) cmp = a.id.localeCompare(b.id);
        break;
      case "speed": {
        const ta = a.estimated_output_tps ?? -Infinity;
        const tb = b.estimated_output_tps ?? -Infinity;
        cmp = ta - tb;
        if (cmp === 0) cmp = a.id.localeCompare(b.id);
        break;
      }
    }
    return cmp * sign;
  });
  return sorted;
}
