import { isBoolean, isNumber, isString } from "es-toolkit";

import type { ConfigValidationIssue } from "../../schema/issues";
import type { CheckerValidationInput } from "../types";

import {
  isPlainRecord,
  validateRawContentExpectations,
  validateRawKeyedExpectations,
  validateRawValueExpectation,
} from "../../expect/validate";
import { issue, joinPath } from "../../schema/issues";

const ALLOWED_MODES = new Set(["http", "stream"]);
const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
const ALLOWED_PROVIDERS = new Set(["anthropic", "openai", "openai-responses"]);

export function validateLlmConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
  const issues: ConfigValidationIssue[] = [];
  const defaults =
    isPlainRecord(input.defaults) && isPlainRecord(input.defaults["llm"]) ? input.defaults["llm"] : undefined;

  if (defaults) {
    issues.push(...validateLlmDefaults(defaults, "defaults.llm"));
  }

  for (let i = 0; i < input.targets.length; i++) {
    const target = input.targets[i] as unknown;
    if (!isPlainRecord(target)) continue;
    if (target["type"] !== "llm") continue;
    issues.push(...validateLlmTarget(target, `targets[${i}]`));
  }

  return issues;
}

function getTargetName(target: Record<string, unknown>): string | undefined {
  if (isString(target["name"])) return target["name"];
  return isString(target["id"]) ? target["id"] : undefined;
}

function validateLlmDefaults(defaults: Record<string, unknown>, path: string): ConfigValidationIssue[] {
  const issues: ConfigValidationIssue[] = [];

  if (defaults["mode"] !== undefined && !ALLOWED_MODES.has(defaults["mode"] as string)) {
    issues.push(issue("invalid-type", joinPath(path, "mode"), "必须为 http 或 stream"));
  }
  if (defaults["ignoreSSL"] !== undefined && !isBoolean(defaults["ignoreSSL"])) {
    issues.push(issue("invalid-type", joinPath(path, "ignoreSSL"), "必须为布尔值"));
  }
  if (defaults["headers"] !== undefined) {
    issues.push(...validateStringMap(defaults["headers"], joinPath(path, "headers")));
  }
  if (defaults["options"] !== undefined) {
    issues.push(...validateLlmOptions(defaults["options"], joinPath(path, "options")));
  }
  if (defaults["providerOptions"] !== undefined) {
    issues.push(...validateProviderOptions(defaults["providerOptions"], joinPath(path, "providerOptions")));
  }

  return issues;
}

function validateLlmExpect(
  target: Record<string, unknown>,
  path: string,
  mode: string | undefined,
  targetName?: string,
): ConfigValidationIssue[] {
  const expect = target["expect"];
  if (expect === undefined || expect === null || !isPlainRecord(expect)) return [];
  const issues: ConfigValidationIssue[] = [];
  const expectPath = joinPath(path, "expect");

  if (Array.isArray(expect["status"])) {
    issues.push(...validateStatusValues(expect["status"], joinPath(expectPath, "status"), targetName));
  }
  if (expect["headers"] !== undefined) {
    issues.push(
      ...validateRawKeyedExpectations(expect["headers"], joinPath(expectPath, "headers"), targetName, {
        caseInsensitive: true,
      }),
    );
  }
  if (expect["output"] !== undefined) {
    issues.push(...validateRawContentExpectations(expect["output"], joinPath(expectPath, "output"), targetName));
  }
  if (expect["finishReason"] !== undefined) {
    issues.push(
      ...validateRawValueExpectation(expect["finishReason"], joinPath(expectPath, "finishReason"), targetName),
    );
  }
  if (expect["rawFinishReason"] !== undefined) {
    issues.push(
      ...validateRawValueExpectation(expect["rawFinishReason"], joinPath(expectPath, "rawFinishReason"), targetName),
    );
  }
  if (expect["usage"] !== undefined) {
    issues.push(...validateUsageExpect(expect["usage"], joinPath(expectPath, "usage"), targetName));
  }
  if (expect["stream"] !== undefined) {
    if (mode === "http") {
      issues.push(
        issue("invalid-type", joinPath(expectPath, "stream"), "expect.stream 仅支持 stream mode", targetName),
      );
    } else {
      issues.push(...validateStreamExpect(expect["stream"], joinPath(expectPath, "stream"), targetName));
    }
  }
  if (expect["durationMs"] !== undefined) {
    issues.push(...validateRawValueExpectation(expect["durationMs"], joinPath(expectPath, "durationMs"), targetName));
  }

  const allowedKeys = new Set([
    "durationMs",
    "finishReason",
    "headers",
    "output",
    "rawFinishReason",
    "status",
    "stream",
    "usage",
  ]);
  for (const key of Object.keys(expect)) {
    if (!allowedKeys.has(key)) issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName));
  }

  return issues;
}

function validateLlmOptions(options: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
  if (!isPlainRecord(options)) return [issue("invalid-type", path, "必须为对象", targetName)];
  const issues: ConfigValidationIssue[] = [];

  if (options["maxOutputTokens"] !== undefined) {
    if (
      !isNumber(options["maxOutputTokens"]) ||
      !Number.isInteger(options["maxOutputTokens"]) ||
      options["maxOutputTokens"] < 1
    ) {
      issues.push(issue("invalid-type", joinPath(path, "maxOutputTokens"), "必须为正整数", targetName));
    }
  }

  for (const key of ["temperature", "topP", "topK", "presencePenalty", "frequencyPenalty", "seed"]) {
    if (options[key] !== undefined && (!isNumber(options[key]) || !Number.isFinite(options[key]))) {
      issues.push(issue("invalid-type", joinPath(path, key), "必须为有限数字", targetName));
    }
  }

  if (options["stopSequences"] !== undefined) {
    if (!Array.isArray(options["stopSequences"])) {
      issues.push(issue("invalid-type", joinPath(path, "stopSequences"), "必须为字符串数组", targetName));
    } else {
      for (let i = 0; i < options["stopSequences"].length; i++) {
        if (!isString(options["stopSequences"][i])) {
          issues.push(issue("invalid-type", `${joinPath(path, "stopSequences")}[${i}]`, "必须为字符串", targetName));
        }
      }
    }
  }

  return issues;
}

function validateLlmTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
  const issues: ConfigValidationIssue[] = [];
  const targetName = getTargetName(target);
  const llm = target["llm"];
  if (!isPlainRecord(llm)) {
    issues.push(issue("required", joinPath(path, "llm"), "缺少 llm 配置", targetName));
    issues.push(...validateLlmExpect(target, path, undefined, targetName));
    return issues;
  }

  if (!isString(llm["provider"]) || !ALLOWED_PROVIDERS.has(llm["provider"])) {
    issues.push(
      issue(
        "invalid-type",
        joinPath(joinPath(path, "llm"), "provider"),
        "必须为 openai、openai-responses 或 anthropic",
        targetName,
      ),
    );
  }

  if (!isString(llm["url"]) || llm["url"].trim() === "") {
    issues.push(issue("required", joinPath(joinPath(path, "llm"), "url"), "缺少 llm.url 字段", targetName));
  } else {
    try {
      const url = new URL(llm["url"]);
      if (!ALLOWED_PROTOCOLS.has(url.protocol)) {
        issues.push(
          issue(
            "invalid-url",
            joinPath(joinPath(path, "llm"), "url"),
            "格式不合法，必须以 http:// 或 https:// 开头",
            targetName,
          ),
        );
      }
    } catch {
      issues.push(issue("invalid-url", joinPath(joinPath(path, "llm"), "url"), "格式不合法", targetName));
    }
  }

  if (!isString(llm["model"]) || llm["model"].trim() === "") {
    issues.push(issue("required", joinPath(joinPath(path, "llm"), "model"), "必须为非空字符串", targetName));
  }
  if (!isString(llm["prompt"]) || llm["prompt"].trim() === "") {
    issues.push(issue("required", joinPath(joinPath(path, "llm"), "prompt"), "必须为非空字符串", targetName));
  }
  if (llm["mode"] !== undefined && !ALLOWED_MODES.has(llm["mode"] as string)) {
    issues.push(issue("invalid-type", joinPath(joinPath(path, "llm"), "mode"), "必须为 http 或 stream", targetName));
  }
  if (llm["headers"] !== undefined) {
    issues.push(...validateStringMap(llm["headers"], joinPath(joinPath(path, "llm"), "headers"), targetName));
  }
  if (llm["ignoreSSL"] !== undefined && !isBoolean(llm["ignoreSSL"])) {
    issues.push(issue("invalid-type", joinPath(joinPath(path, "llm"), "ignoreSSL"), "必须为布尔值", targetName));
  }

  const provider = llm["provider"] as string | undefined;
  if (llm["authToken"] !== undefined && provider !== "anthropic") {
    issues.push(
      issue(
        "invalid-auth",
        joinPath(joinPath(path, "llm"), "authToken"),
        "authToken 仅支持 anthropic provider",
        targetName,
      ),
    );
  }
  if (
    provider === "anthropic" &&
    isString(llm["key"]) &&
    llm["key"].trim() !== "" &&
    isString(llm["authToken"]) &&
    llm["authToken"].trim() !== ""
  ) {
    issues.push(
      issue("auth-conflict", joinPath(joinPath(path, "llm"), "key"), "key 与 authToken 不能同时配置", targetName),
    );
  }
  if (llm["options"] !== undefined) {
    issues.push(...validateLlmOptions(llm["options"], joinPath(joinPath(path, "llm"), "options"), targetName));
  }
  if (llm["providerOptions"] !== undefined) {
    issues.push(
      ...validateProviderOptions(
        llm["providerOptions"],
        joinPath(joinPath(path, "llm"), "providerOptions"),
        targetName,
      ),
    );
  }

  const mode = (llm["mode"] as string | undefined) ?? "http";
  issues.push(...validateLlmExpect(target, path, mode, targetName));

  return issues;
}

function validateProviderOptions(value: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
  if (!isPlainRecord(value)) return [issue("invalid-type", path, "必须为 JSON object", targetName)];
  return [];
}

function validateStatusValues(values: unknown[], path: string, targetName?: string): ConfigValidationIssue[] {
  const issues: ConfigValidationIssue[] = [];
  for (let i = 0; i < values.length; i++) {
    const value = values[i];
    const itemPath = `${path}[${i}]`;
    if (isNumber(value)) {
      if (!Number.isInteger(value) || value < 100 || value > 599) {
        issues.push(issue("invalid-status", itemPath, "status 数字必须为 100-599 之间的整数", targetName));
      }
      continue;
    }
    if (isString(value)) {
      if (!/^[1-5]xx$/.test(value)) {
        issues.push(issue("invalid-status", itemPath, "status 模式必须为 1xx 到 5xx", targetName));
      }
      continue;
    }
    issues.push(issue("invalid-status", itemPath, "status 必须为整数或 1xx 到 5xx 模式", targetName));
  }
  return issues;
}

function validateStreamExpect(stream: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
  if (!isPlainRecord(stream)) return [issue("invalid-type", path, "必须为对象", targetName)];
  const issues: ConfigValidationIssue[] = [];

  if (stream["completed"] !== undefined && !isBoolean(stream["completed"])) {
    issues.push(issue("invalid-type", joinPath(path, "completed"), "必须为布尔值", targetName));
  }
  if (stream["firstTokenMs"] !== undefined) {
    issues.push(...validateRawValueExpectation(stream["firstTokenMs"], joinPath(path, "firstTokenMs"), targetName));
  }

  const allowedKeys = new Set(["completed", "firstTokenMs"]);
  for (const key of Object.keys(stream)) {
    if (!allowedKeys.has(key)) issues.push(issue("unknown-field", joinPath(path, key), "是未知字段", targetName));
  }

  return issues;
}

function validateStringMap(value: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
  if (!isPlainRecord(value)) return [issue("invalid-type", path, "必须为对象", targetName)];
  const issues: ConfigValidationIssue[] = [];
  for (const [key, val] of Object.entries(value)) {
    if (!isString(val)) {
      issues.push(issue("invalid-type", joinPath(path, key), "必须为字符串", targetName));
    }
  }
  return issues;
}

function validateUsageExpect(usage: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
  if (!isPlainRecord(usage)) return [issue("invalid-type", path, "必须为对象", targetName)];
  const issues: ConfigValidationIssue[] = [];

  for (const key of ["inputTokens", "outputTokens", "totalTokens"]) {
    if (usage[key] !== undefined) {
      issues.push(...validateRawValueExpectation(usage[key], joinPath(path, key), targetName));
    }
  }

  const allowedKeys = new Set(["inputTokens", "outputTokens", "totalTokens"]);
  for (const key of Object.keys(usage)) {
    if (!allowedKeys.has(key)) issues.push(issue("unknown-field", joinPath(path, key), "是未知字段", targetName));
  }

  return issues;
}
