/**
 * Pure, framework-free helpers for the local model server feature (F35, V2 with
 * the Hugging Face source). Draft validation mirrors the backend invariants
 * (`ModelServerEndpoint::new`, `ModelPath::new`, `HfModelRef::new`,
 * `LlamaCppOptions::new`, `LocalModelServerConfig::new`) so the form can surface
 * errors before any `invoke`. No React, no Tauri here.
 */

import type { LocalModelServerConfig, ModelSource } from "@/domain";

/** A field-keyed validation error map (empty ⇒ valid). */
export type ModelServerErrors = Partial<
  Record<
    "name" | "baseURL" | "port" | "servedModelName" | "modelSource" | "host",
    string
  >
>;

/** Which source tab is active in the editor (kept even when the field is blank). */
export type ModelSourceKind = "huggingFace" | "localPath";

/** Whether a string is a valid `http://`/`https://` URL (mirror of backend). */
function isValidHttpUrl(v: string): boolean {
  if (v.trim().length === 0) return false;
  return v.startsWith("http://") || v.startsWith("https://");
}

/**
 * Whether a path looks absolute (mirror of the backend `is_absolute_path`:
 * POSIX `/…` or a Windows drive/UNC). Blank is handled by the caller.
 */
export function isAbsolutePath(path: string): boolean {
  if (path.startsWith("/") || path.startsWith("\\")) return true;
  // Windows drive `C:/` or `C:\`.
  return /^[a-zA-Z]:[/\\]/.test(path);
}

/**
 * Whether a Hugging Face reference is well-formed (mirror of the backend
 * `HfModelRef::new`): `namespace/repo` or `namespace/repo:quant`, no spaces, not
 * an URL, at most one `:`. Blank is handled by the caller.
 */
export function isValidHfRef(repo: string): boolean {
  if (/\s/.test(repo)) return false;
  if (repo.startsWith("http://") || repo.startsWith("https://")) return false;
  if ((repo.match(/:/g) ?? []).length > 1) return false;
  const [base, quant] = repo.includes(":")
    ? [repo.slice(0, repo.indexOf(":")), repo.slice(repo.indexOf(":") + 1)]
    : [repo, undefined];
  if (quant !== undefined && quant.length === 0) return false;
  const slash = base.split("/");
  return slash.length === 2 && slash[0].length > 0 && slash[1].length > 0;
}

/**
 * Validates a {@link LocalModelServerConfig} draft the way the backend would.
 * Returns an empty object when the draft is valid.
 */
export function validateModelServer(c: LocalModelServerConfig): ModelServerErrors {
  const errors: ModelServerErrors = {};
  if (c.name.trim().length === 0) errors.name = "Name is required.";
  if (!isValidHttpUrl(c.baseURL)) {
    errors.baseURL = "Base URL must start with http:// or https://.";
  }
  if (!Number.isInteger(c.port) || c.port < 1 || c.port > 65_535) {
    errors.port = "Port must be an integer between 1 and 65535.";
  }
  if (c.servedModelName.trim().length === 0) {
    errors.servedModelName = "Le nom du modèle est requis.";
  }
  if (c.host.trim().length === 0 || /\s/.test(c.host)) {
    errors.host = "L'adresse d'écoute est requise et ne doit pas contenir d'espace.";
  }

  const source = c.modelSource;
  if (source) {
    if (source.type === "localPath") {
      if (source.path.trim().length === 0) {
        errors.modelSource = "Le chemin du fichier .gguf est requis.";
      } else if (!isAbsolutePath(source.path.trim())) {
        errors.modelSource = "Le chemin du fichier doit être absolu.";
      }
    } else if (source.repo.trim().length === 0) {
      errors.modelSource = "Le modèle Hugging Face est requis.";
    } else if (!isValidHfRef(source.repo.trim())) {
      errors.modelSource =
        "Format attendu : auteur/modèle ou auteur/modèle:quant (ex. unsloth/Qwen3.5-9B-GGUF:Q4_K_M).";
    }
  }

  // The backend rejects auto-start without a model source.
  if (c.autoStart && !source) {
    errors.modelSource =
      "Le démarrage automatique nécessite une source de modèle.";
  }
  return errors;
}

/** Whether a draft is valid (no errors). */
export function isModelServerValid(c: LocalModelServerConfig): boolean {
  return Object.keys(validateModelServer(c)).length === 0;
}

/** Generates a UUID for a client-created server (crypto when available). */
export function newModelServerId(): string {
  const c = globalThis.crypto as Crypto | undefined;
  if (c && typeof c.randomUUID === "function") return c.randomUUID();
  return `srv-${Math.random().toString(36).slice(2, 10)}`;
}

/**
 * A fresh model-server draft (id minted client-side) pre-filled with a sensible
 * local `llama-server` endpoint, so the form starts valid apart from the name
 * and the (still empty) model source.
 */
export function emptyModelServer(): LocalModelServerConfig {
  return {
    id: newModelServerId(),
    kind: "llamaCpp",
    name: "",
    baseURL: "http://localhost:8080/v1",
    port: 8080,
    servedModelName: "",
    host: "127.0.0.1",
    jinja: false,
    args: [],
    autoStart: false,
    stopPolicy: "stopOnAppExit",
  };
}

/** Parses a whitespace-separated args string into a trimmed, non-empty list. */
export function parseArgs(raw: string): string[] {
  return raw
    .split(/\s+/)
    .map((s) => s.trim())
    .filter((s) => s.length > 0);
}

/**
 * Derives a default served-model name from a source when the user has not typed
 * one yet: the file stem for a local path, or the repo name (without namespace
 * and quant) for a Hugging Face reference. Returns `""` when nothing usable.
 */
export function deriveServedModelName(source: ModelSource | undefined): string {
  if (!source) return "";
  if (source.type === "localPath") {
    const path = source.path.trim();
    if (path.length === 0) return "";
    const base = path.split(/[/\\]/).pop() ?? "";
    return base.replace(/\.gguf$/i, "");
  }
  const repo = source.repo.trim();
  if (repo.length === 0) return "";
  const base = repo.includes(":") ? repo.slice(0, repo.indexOf(":")) : repo;
  return base.split("/").pop() ?? "";
}

/**
 * llama.cpp flags already piloted by dedicated editor fields. Typing any of them
 * (bare or as `--flag=value`) in the free "extra arguments" field is a
 * conflict: the backend stays the authority, but we mirror a soft warning.
 */
export const RESERVED_LLAMACPP_FLAGS: readonly string[] = [
  "--model",
  "-m",
  "-hf",
  "--port",
  "--host",
  "-ngl",
  "--gpu-layers",
  "-c",
  "--ctx-size",
  "--jinja",
];

/**
 * Returns the reserved flags present in a free-form args list (matching bare
 * tokens and the `--flag=value` form), or an empty list when none clash.
 */
export function reservedFlagsIn(args: string[]): string[] {
  const reserved = new Set(RESERVED_LLAMACPP_FLAGS);
  const hits = new Set<string>();
  for (const token of args) {
    const flag = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
    if (reserved.has(flag)) hits.add(flag);
  }
  return [...hits];
}
