/**
 * Shared correlation of an agent to its local model server's launch state
 * (F35 / ticket #54). One place owns the chain
 * `agentId → profileId → opencode.localModelServerId → statusByServer[serverId]`
 * so both the {@link AgentsPanel} badge and the full-cell overlay in
 * `LayoutGrid`/`LeafView` read the same truth instead of duplicating it.
 *
 * Pure data + a thin `useGateways`-backed hook: no Tauri names here (the event
 * stream and profile list come through the gateways/adapters).
 */

import { useCallback, useEffect, useState } from "react";

import type { Agent, AgentProfile, ModelServerStatus } from "@/domain";
import { useGateways } from "@/app/di";

/**
 * Correlate a pinned agent to its model server's current lifecycle status, if
 * any. Returns `undefined` when the agent's profile binds no local model server
 * or when that server has emitted no status yet.
 */
export function correlateModelServerStatus(
  agent: Pick<Agent, "profileId"> | undefined,
  profiles: AgentProfile[],
  statusByServer: Record<string, ModelServerStatus>,
): ModelServerStatus | undefined {
  if (!agent) return undefined;
  const serverId = profiles.find((p) => p.id === agent.profileId)?.opencode
    ?.localModelServerId;
  return serverId ? statusByServer[serverId] : undefined;
}

/**
 * The full-cell overlay text for a model server that is still preparing, or
 * `null` when no overlay should show. Only the "not yet usable" states raise a
 * veil — `ready`/`failed`/`notConfigured` (and an absent status) return `null`:
 * - `downloading` → "Téléchargement du modèle…",
 * - `starting`/`probing` → "Chargement du serveur…".
 *
 * This is the single predicate that decides whether the launch overlay covers a
 * cell (and supplies its title). The download *progress* (bar/%/bytes, F2) is a
 * separate, additive concern — see {@link describeModelServerDownload}.
 */
export function modelServerOverlayText(
  status: ModelServerStatus | undefined,
): string | null {
  if (!status) return null;
  switch (status.state) {
    case "downloading":
      return "Téléchargement du modèle…";
    case "starting":
    case "probing":
      return "Chargement du serveur…";
    default:
      return null;
  }
}

/**
 * Human-readable byte size, decimal (SI) units so it matches what download UIs
 * and llama.cpp report (`1 Mo = 1000 Ko`). Bytes are shown whole; larger units
 * keep one decimal, trailing `.0` trimmed. Guards against non-finite/negative
 * inputs (→ "0 o").
 */
export function formatBytes(n: number): string {
  if (!Number.isFinite(n) || n <= 0) return "0 o";
  const units = ["o", "Ko", "Mo", "Go", "To"];
  let value = n;
  let unit = 0;
  while (value >= 1000 && unit < units.length - 1) {
    value /= 1000;
    unit += 1;
  }
  const rounded =
    unit === 0 ? Math.round(value) : Math.round(value * 10) / 10;
  return `${rounded} ${units[unit]}`;
}

/**
 * Download progress detail extracted from a `downloading` status (F2), or `null`
 * for any other state (`starting`/`probing`/`ready`/`failed` → no bar). Mirrors
 * the wire shape without inventing data:
 * - `percent`: the backend's completion % clamped to `0..100` when the total is
 *   known, else `null` → the overlay shows an **indeterminate** bar (no fake %);
 * - `bytesLabel`: `"X Mo / Y Mo"` when both bounds are known, `"X Mo"` when only
 *   the downloaded amount is, else `null`;
 * - `source`: the remote model ref (e.g. HF `namespace/repo`) when present.
 */
export interface ModelServerDownloadProgress {
  percent: number | null;
  bytesLabel: string | null;
  source: string | null;
}

export function describeModelServerDownload(
  status: ModelServerStatus | undefined,
): ModelServerDownloadProgress | null {
  if (!status || status.state !== "downloading") return null;
  const { downloadedBytes, totalBytes, percent, source } = status;
  const bytesLabel =
    downloadedBytes != null && totalBytes != null
      ? `${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)}`
      : downloadedBytes != null
        ? formatBytes(downloadedBytes)
        : null;
  return {
    percent: percent != null ? Math.min(100, Math.max(0, percent)) : null,
    bytesLabel,
    source: source ?? null,
  };
}

/** What {@link useModelServerLaunchState} exposes to a consumer. */
export interface ModelServerLaunchState {
  /**
   * The correlated model-server status for a pinned agent (or `undefined` when
   * it has no bound server / no status yet). Stable across renders unless the
   * profiles or the per-server status map change.
   */
  statusForAgent: (
    agent: Pick<Agent, "profileId"> | undefined,
  ) => ModelServerStatus | undefined;
}

/**
 * Self-contained correlation source for consumers that do NOT already hold the
 * `useAgents` view-model (e.g. `LeafView`). Loads the profiles once and folds
 * every `modelServerStatusChanged` event into a per-server status map, exactly
 * like `useAgents` does — reusing {@link correlateModelServerStatus} for the
 * lookup. A profile change (which may rebind the server) re-pulls the profiles.
 *
 * `profile`/`system` may be absent in unit tests that inject a partial gateway
 * set; both are guarded, in which case the status is simply never found.
 */
export function useModelServerLaunchState(
  _projectId: string,
): ModelServerLaunchState {
  const { profile, system } = useGateways();
  const [profiles, setProfiles] = useState<AgentProfile[]>([]);
  const [statusByServer, setStatusByServer] = useState<
    Record<string, ModelServerStatus>
  >({});

  // Load (and reload) the profiles: the `profileId → localModelServerId`
  // binding lives on the profile, so we need the current list to correlate.
  const reloadProfiles = useCallback(() => {
    if (!profile) return;
    profile
      .listProfiles()
      .then(setProfiles)
      .catch(() => {
        /* ignore — no binding resolvable, overlay stays off */
      });
  }, [profile]);

  useEffect(() => {
    reloadProfiles();
  }, [reloadProfiles]);

  // Fold the model-server lifecycle stream, keyed by server id — the same
  // reduction `useAgents` performs. A profile change may rebind the server, so
  // re-pull the profiles too.
  useEffect(() => {
    if (!system) return;
    let unsubscribe: (() => void) | undefined;
    let cancelled = false;
    void system
      .onDomainEvent((event) => {
        if (event.type === "modelServerStatusChanged") {
          setStatusByServer((prev) => ({
            ...prev,
            [event.serverId]: event.status,
          }));
        } else if (event.type === "agentProfileChanged") {
          reloadProfiles();
        }
      })
      .then((un) => {
        if (cancelled) un();
        else unsubscribe = un;
      });
    return () => {
      cancelled = true;
      unsubscribe?.();
    };
  }, [system, reloadProfiles]);

  const statusForAgent = useCallback(
    (agent: Pick<Agent, "profileId"> | undefined) =>
      correlateModelServerStatus(agent, profiles, statusByServer),
    [profiles, statusByServer],
  );

  return { statusForAgent };
}
