/**
 * Pure, framework-free helpers for the first-run wizard (testable without React
 * or Tauri, ARCHITECTURE §1.3). Validation of a custom/edited profile and small
 * factories live here; the components only render.
 */

import type {
  AgentProfile,
  ContextInjection,
  HttpChatConfig,
  InjectionStrategy,
  OpenCodeConfig,
} from "@/domain";

/** A field-keyed validation error map (empty ⇒ valid). */
export type ProfileErrors = Partial<
  Record<
    | "name"
    | "command"
    | "target"
    | "flag"
    | "var"
    | "endpoint"
    | "baseURL"
    | "model"
    | "apiKeyEnv"
    | "requestTimeoutMs"
    | "connectTimeoutMs"
    | "maxToolIterations",
    string
  >
>;

/** Whether a path is a relative, traversal-free file name (mirror of backend). */
export function isRelativeSafe(path: string): boolean {
  if (path.length === 0) return false;
  if (path.startsWith("/") || path.startsWith("\\")) return false;
  // Windows drive (C:) / UNC.
  if (/^[a-zA-Z]:/.test(path)) return false;
  return !path.split(/[/\\]/).includes("..");
}

/** Whether a string is a valid environment-variable identifier. */
export function isValidEnvVar(v: string): boolean {
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(v);
}

/**
 * Whether a string is a valid `http://`/`https://` endpoint (mirror of the
 * backend `HttpChatConfig::new`: non-empty once trimmed, and the raw value
 * starts with `http://` or `https://`).
 */
export function isValidHttpUrl(v: string): boolean {
  if (v.trim().length === 0) return false;
  return v.startsWith("http://") || v.startsWith("https://");
}

/**
 * Whether a value is an acceptable positive-integer timeout/guard (mirror of the
 * backend, which rejects `0` and only ever holds `u32`/`u16`). `undefined` means
 * "left blank" and is valid (the backend applies its own default).
 */
function isValidPositiveInt(value: number | undefined, max: number): boolean {
  if (value === undefined) return true;
  return Number.isInteger(value) && value > 0 && value <= max;
}

const MAX_U32 = 0xffff_ffff;
const MAX_U16 = 0xffff;

/**
 * Validates an {@link HttpChatConfig} draft the way the backend
 * `HttpChatConfig::new` would, so the wizard can surface errors before any
 * `invoke`. Returns an empty object when the draft is valid.
 */
export function validateHttpChatConfig(c: HttpChatConfig): ProfileErrors {
  const errors: ProfileErrors = {};
  if (!isValidHttpUrl(c.endpoint)) {
    errors.endpoint = "Endpoint must start with http:// or https://.";
  }
  if (c.model.trim().length === 0) {
    errors.model = "Model is required.";
  }
  // The API key field holds the NAME of an environment variable, never a key.
  if (c.apiKeyEnv !== undefined && c.apiKeyEnv.length > 0 && !isValidEnvVar(c.apiKeyEnv)) {
    errors.apiKeyEnv = "Must be a valid env var name (not the key itself).";
  }
  if (!isValidPositiveInt(c.requestTimeoutMs, MAX_U32)) {
    errors.requestTimeoutMs = "Must be a positive integer (ms).";
  }
  if (!isValidPositiveInt(c.connectTimeoutMs, MAX_U32)) {
    errors.connectTimeoutMs = "Must be a positive integer (ms).";
  }
  if (!isValidPositiveInt(c.maxToolIterations, MAX_U16)) {
    errors.maxToolIterations = "Must be a positive integer.";
  }
  return errors;
}

/**
 * Validates an {@link OpenCodeConfig} draft the way the backend
 * `OpenCodeConfig::new` would (base URL http/https + non-empty, model non-empty;
 * the API key is a free-form, optional secret). Returns an empty object when the
 * draft is valid.
 */
export function validateOpenCodeConfig(c: OpenCodeConfig): ProfileErrors {
  const errors: ProfileErrors = {};
  if (!isValidHttpUrl(c.baseURL)) {
    errors.baseURL = "Base URL must start with http:// or https://.";
  }
  if (c.model.trim().length === 0) {
    errors.model = "Model is required.";
  }
  return errors;
}

/**
 * Validates a profile draft the way the backend would, so the wizard can surface
 * errors before any `invoke`. Returns an empty object when the draft is valid.
 */
export function validateProfile(p: AgentProfile): ProfileErrors {
  const errors: ProfileErrors = {};
  if (p.name.trim().length === 0) errors.name = "Name is required.";
  if (p.command.trim().length === 0) errors.command = "Command is required.";

  const ci = p.contextInjection;
  if (ci.strategy === "conventionFile") {
    if (!isRelativeSafe(ci.target)) {
      errors.target = "Target must be a relative file name (no .. or absolute).";
    }
  } else if (ci.strategy === "flag") {
    if (ci.flag.trim().length === 0) errors.flag = "Flag is required.";
  } else if (ci.strategy === "env") {
    if (!isValidEnvVar(ci.var)) errors.var = "Must be a valid env var identifier.";
  }

  // An OpenAI-compatible profile additionally carries an HTTP chat config; the
  // backend refuses to persist it unless it is well-formed, so mirror that here.
  if (p.structuredAdapter === "openAiCompatible") {
    if (!p.chatHttp) {
      errors.endpoint = "Endpoint must start with http:// or https://.";
      errors.model = "Model is required.";
    } else {
      Object.assign(errors, validateHttpChatConfig(p.chatHttp));
    }
  }
  // An OpenCode profile carries its own llama.cpp endpoint config (local mode)
  // or a cloud provider config (`opencodeProvider`, ticket #92) — never both,
  // and the cloud sub-form owns its own submit-time validation (provider,
  // model, API key), so only the local-mode shape is mirrored here.
  if (p.structuredAdapter === "openCode" && !p.opencodeProvider) {
    if (!p.opencode) {
      errors.baseURL = "Base URL must start with http:// or https://.";
      errors.model = "Model is required.";
    } else {
      Object.assign(errors, validateOpenCodeConfig(p.opencode));
    }
  }
  return errors;
}

/** Whether a profile draft is valid (no errors). */
export function isProfileValid(p: AgentProfile): boolean {
  return Object.keys(validateProfile(p)).length === 0;
}

/**
 * Builds a default {@link ContextInjection} for a strategy, so switching the
 * strategy dropdown produces a sensible editable shape.
 */
export function defaultInjection(strategy: InjectionStrategy): ContextInjection {
  switch (strategy) {
    case "conventionFile":
      return { strategy, target: "CONTEXT.md" };
    case "flag":
      return { strategy, flag: "--context-file {path}" };
    case "env":
      return { strategy, var: "AGENT_CONTEXT_FILE" };
    case "stdin":
      return { strategy };
  }
}

/**
 * A sensible default {@link HttpChatConfig} for a fresh OpenAI-compatible
 * profile draft — pre-fills a local Ollama endpoint so the form starts valid.
 * Mirrors the backend reference profile (`http://localhost:11434/v1`).
 */
export function defaultHttpChatConfig(): HttpChatConfig {
  return {
    endpoint: "http://localhost:11434/v1",
    model: "qwen2.5-coder",
    requestTimeoutMs: 120_000,
    connectTimeoutMs: 5_000,
    maxToolIterations: 16,
  };
}

/**
 * A sensible default {@link OpenCodeConfig} for a fresh OpenCode profile draft —
 * pre-fills a local `llama-server` endpoint so the form starts valid. Mirrors the
 * backend reference profile (`http://localhost:8080/v1`, `qwen3-coder-30b`).
 */
export function defaultOpenCodeConfig(): OpenCodeConfig {
  return {
    baseURL: "http://localhost:8080/v1",
    apiKey: "sk-no-key",
    model: "qwen3-coder-30b",
  };
}

/** A fresh, empty custom-profile draft (id minted client-side). */
export function emptyCustomProfile(): AgentProfile {
  return {
    id: newProfileId(),
    name: "",
    command: "",
    args: [],
    contextInjection: { strategy: "conventionFile", target: "CONTEXT.md" },
    detect: null,
    cwdTemplate: "{projectRoot}",
  };
}

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

/** 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);
}
