import { 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";

export function validateDbConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
  const issues: ConfigValidationIssue[] = [];

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

  return issues;
}

function collectRowExpects(rows: unknown[], path: string, targetName?: string): ConfigValidationIssue[] {
  const issues: ConfigValidationIssue[] = [];
  for (let i = 0; i < rows.length; i++) {
    const row = rows[i]!;
    if (!isPlainRecord(row)) {
      issues.push(issue("invalid-type", `${path}[${i}]`, "必须为对象", targetName));
      continue;
    }
    issues.push(...validateRawKeyedExpectations(row, `${path}[${i}]`, targetName));
  }
  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 validateDbExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
  const targetName = getTargetName(target);
  const expect = target["expect"];
  if (expect === undefined || expect === null || !isPlainRecord(expect)) return [];
  const issues: ConfigValidationIssue[] = [];
  const expectPath = joinPath(path, "expect");

  if (expect["durationMs"] !== undefined) {
    issues.push(...validateRawValueExpectation(expect["durationMs"], joinPath(expectPath, "durationMs"), targetName));
  }

  if (expect["rowCount"] !== undefined) {
    issues.push(...validateRawValueExpectation(expect["rowCount"], joinPath(expectPath, "rowCount"), targetName));
  }

  if (expect["rows"] !== undefined) {
    if (!Array.isArray(expect["rows"])) {
      issues.push(issue("invalid-type", joinPath(expectPath, "rows"), "必须为数组", targetName));
    } else {
      issues.push(...collectRowExpects(expect["rows"], joinPath(expectPath, "rows"), targetName));
    }
  }

  if (expect["result"] !== undefined) {
    issues.push(...validateRawContentExpectations(expect["result"], joinPath(expectPath, "result"), targetName));
  }

  const allowedKeys = new Set(["durationMs", "result", "rowCount", "rows"]);
  for (const key of Object.keys(expect)) {
    if (!allowedKeys.has(key)) {
      issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName));
    }
  }

  return issues;
}

function validateDbTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
  const issues: ConfigValidationIssue[] = [];
  const targetName = getTargetName(target);
  const db = target["db"];

  if (!isPlainRecord(db)) {
    issues.push(issue("required", joinPath(path, "db"), "缺少 db.url 字段", targetName));
    issues.push(...validateDbExpect(target, path));
    return issues;
  }

  if (!isString(db["url"]) || db["url"].trim() === "") {
    issues.push(issue("required", joinPath(joinPath(path, "db"), "url"), "缺少 db.url 字段", targetName));
  }

  if (db["query"] !== undefined) {
    if (!isString(db["query"])) {
      issues.push(issue("invalid-type", joinPath(joinPath(path, "db"), "query"), "必须为字符串", targetName));
    } else if (db["query"].trim() === "") {
      issues.push(
        issue(
          "invalid-value",
          joinPath(joinPath(path, "db"), "query"),
          "不能为空字符串（如不需要查询则不配置该字段）",
          targetName,
        ),
      );
    }
  }

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

  issues.push(...validateDbExpect(target, path));
  return issues;
}
