import type { ErrorObject } from "ajv";

import Ajv from "ajv";
import { isPlainObject, isString } from "es-toolkit";

import type { CheckerRegistry } from "../runner/registry";
import type { ConfigValidationIssue } from "./issues";
import type { RawProbeConfig } from "./types";

import { createProbeConfigSchema, createTargetSchema } from "./builder";
import { issue } from "./issues";

export function createConfigAjv(): Ajv {
  return new Ajv({ allErrors: true, coerceTypes: false, removeAdditional: false, strict: true, useDefaults: false });
}

export function issuesFromAjvErrors(errors: ErrorObject[], root: unknown, basePath = ""): ConfigValidationIssue[] {
  return normalizeAjvErrors(errors, basePath).map((error) => issueFromAjvError(error, root, basePath));
}

export function validateProbeConfigContract(
  config: unknown,
  registry: CheckerRegistry,
): { config: null; issues: ConfigValidationIssue[] } | { config: RawProbeConfig; issues: [] } {
  const ajv = createConfigAjv();
  const checkers = registry.definitions;
  const issues: ConfigValidationIssue[] = [];
  const rootValidate = ajv.compile(createProbeConfigSchema(checkers));
  if (!rootValidate(config)) {
    issues.push(...issuesFromAjvErrors(rootValidate.errors ?? [], config));
  }

  if (isPlainObject(config)) {
    const configRecord = config as Record<string, unknown>;
    const targetsValue: unknown = configRecord["targets"];
    if (!Array.isArray(targetsValue))
      return issues.length > 0 ? { config: null, issues } : { config: config as RawProbeConfig, issues: [] };
    const targets = targetsValue;
    for (let i = 0; i < targets.length; i++) {
      const target: unknown = targets[i];
      if (!isPlainObject(target)) continue;
      const targetRecord = target as Record<string, unknown>;
      const targetType: unknown = targetRecord["type"];
      if (!isString(targetType)) continue;
      const checker = registry.tryGet(targetType);
      if (!checker) continue;
      const targetValidate = ajv.compile(createTargetSchema(checker));
      if (!targetValidate(target)) {
        issues.push(...issuesFromAjvErrors(targetValidate.errors ?? [], config, `targets[${i}]`));
      }
    }
  }

  return issues.length > 0 ? { config: null, issues } : { config: config as RawProbeConfig, issues: [] };
}

function buildIssuePath(basePath: string, error: ErrorObject): string {
  const pointerPath = jsonPointerToPath(error.instancePath);
  let path = basePath ? joinBasePath(basePath, pointerPath) : pointerPath;
  if (error.keyword === "required" && "missingProperty" in error.params) {
    path = joinBasePath(path, String(error.params["missingProperty"]));
  }
  if (error.keyword === "additionalProperties" && "additionalProperty" in error.params) {
    path = joinBasePath(path, String(error.params["additionalProperty"]));
  }
  return path;
}

function hasMoreSpecificError(keywords: Set<string>): boolean {
  return ["const", "enum", "maximum", "minimum", "minLength", "pattern"].some((keyword) => keywords.has(keyword));
}

function issueFromAjvError(error: ErrorObject, root: unknown, basePath: string): ConfigValidationIssue {
  const path = buildIssuePath(basePath, error);
  const targetName = targetDisplayNameFromPath(root, path);
  switch (error.keyword) {
    case "additionalProperties":
      return issue("unknown-field", path, "是未知字段", targetName);
    case "const":
    case "enum":
      return issue("invalid-value", path, "不在允许范围内", targetName);
    case "maximum":
    case "minimum":
      return issue("invalid-range", path, "数值范围不合法", targetName);
    case "minLength":
      return issue("invalid-format", path, "不能为空", targetName);
    case "pattern":
      return issue("invalid-format", path, "格式不合法", targetName);
    case "required":
      return issue("required", path, "缺少必填字段", targetName);
    case "type":
      return issue("invalid-type", path, "类型不合法", targetName);
    default:
      return issue("invalid-config", path, error.message ?? "配置不合法", targetName);
  }
}

function joinBasePath(basePath: string, path: string): string {
  if (basePath === "") return path;
  if (path === "") return basePath;
  if (path.startsWith("[")) return `${basePath}${path}`;
  return `${basePath}.${path}`;
}

function jsonPointerToPath(pointer: string): string {
  if (pointer === "") return "";
  return pointer
    .slice(1)
    .split("/")
    .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))
    .reduce((path, part) => (/^\d+$/.test(part) ? `${path}[${part}]` : joinBasePath(path, part)), "");
}

function normalizeAjvErrors(errors: ErrorObject[], basePath: string): ErrorObject[] {
  const nonCompositeErrors = errors.filter((error) => error.keyword !== "anyOf" && error.keyword !== "oneOf");
  const candidates = nonCompositeErrors.length > 0 ? nonCompositeErrors : errors;
  const keywordsByPath = new Map<string, Set<string>>();

  for (const error of candidates) {
    const path = buildIssuePath(basePath, error);
    const keywords = keywordsByPath.get(path) ?? new Set<string>();
    keywords.add(error.keyword);
    keywordsByPath.set(path, keywords);
  }

  const seenValueErrors = new Set<string>();
  return candidates.filter((error) => {
    const path = buildIssuePath(basePath, error);
    const keywords = keywordsByPath.get(path) ?? new Set<string>();
    if (error.keyword === "type" && hasMoreSpecificError(keywords)) return false;
    if (error.keyword === "const" || error.keyword === "enum") {
      if (seenValueErrors.has(path)) return false;
      seenValueErrors.add(path);
    }
    return true;
  });
}

function targetDisplayNameFromPath(root: unknown, path: string): string | undefined {
  const match = /^targets\[(\d+)\]/.exec(path);
  if (!match || !isPlainObject(root)) return undefined;
  const rootRecord = root as Record<string, unknown>;
  const targetsValue: unknown = rootRecord["targets"];
  if (!Array.isArray(targetsValue)) return undefined;
  const target: unknown = targetsValue[Number(match[1])];
  if (!isPlainObject(target)) return undefined;
  const targetRecord = target as Record<string, unknown>;
  const targetName: unknown = targetRecord["name"];
  if (isString(targetName)) return targetName;
  const targetId: unknown = targetRecord["id"];
  return isString(targetId) ? targetId : undefined;
}
