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

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

import { issue, joinPath } from "./schema/issues";

const VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
const VARIABLE_REFERENCE_PATTERN = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?:\|([^}]*))?\}/g;
const COMPLETE_VARIABLE_REFERENCE_PATTERN = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?:\|([^}]*))?\}$/;
const ESCAPED_VARIABLE_PATTERN = /\$\$\{([^}]*)\}/g;

interface VariableReference {
  defaultValue?: string;
  key: string;
}

interface VariableResolutionIssueContext {
  path: string;
  targetId?: string;
  targetName?: string;
}

export function extractVariables(config: unknown): {
  issues: ConfigValidationIssue[];
  variables: Map<string, VariableValue>;
} {
  const issues: ConfigValidationIssue[] = [];
  const variables = new Map<string, VariableValue>();

  if (!isPlainObject(config)) {
    return { issues, variables };
  }
  const configRecord = config as Record<string, unknown>;
  if (configRecord["variables"] === undefined) {
    return { issues, variables };
  }

  const rawVariables: unknown = configRecord["variables"];
  if (!isPlainObject(rawVariables)) {
    issues.push(issue("invalid-type", "variables", "必须为对象"));
    return { issues, variables };
  }

  for (const [key, value] of Object.entries(rawVariables as Record<string, unknown>)) {
    const path = joinPath("variables", key);
    if (!VARIABLE_NAME_PATTERN.test(key)) {
      issues.push(issue("invalid-format", path, "变量名不符合命名规则"));
      continue;
    }
    if (!isVariableValue(value)) {
      issues.push(issue("invalid-type", path, `变量值不允许为 ${describeInvalidVariableValue(value)}`));
      continue;
    }
    variables.set(key, value);
  }

  return { issues, variables };
}

export function resolveVariables(config: unknown): { config: unknown; issues: ConfigValidationIssue[] } {
  const { issues, variables } = extractVariables(config);
  if (!isPlainObject(config)) {
    return { config, issues };
  }
  const configRecord = config as Record<string, unknown>;
  const rawTargets: unknown = configRecord["targets"];
  if (!Array.isArray(rawTargets)) {
    return { config, issues };
  }

  const targets = rawTargets.map((target, index) => resolveTargetVariables(target, index, variables, issues));
  return { config: { ...config, targets }, issues };
}

function describeInvalidVariableValue(value: unknown): string {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  return typeof value;
}

function inferStringValue(value: string): VariableValue {
  const numberValue = Number(value);
  if (Number.isFinite(numberValue)) return numberValue;
  if (value === "true") return true;
  if (value === "false") return false;
  return value;
}

function isVariableValue(value: unknown): value is VariableValue {
  return isString(value) || isNumber(value) || isBoolean(value);
}

function parseVariableReference(match: RegExpExecArray): VariableReference {
  return { defaultValue: match[2], key: match[1]! };
}

function replaceStringValue(
  value: string,
  variables: Map<string, VariableValue>,
  issues: ConfigValidationIssue[],
  context: VariableResolutionIssueContext,
): string | VariableValue {
  const trimmed = value.trim();
  const completeMatch = COMPLETE_VARIABLE_REFERENCE_PATTERN.exec(trimmed);
  if (completeMatch) {
    const resolved = resolveVariableReference(parseVariableReference(completeMatch), variables, issues, context);
    return resolved ?? value;
  }

  const escaped: string[] = [];
  const protectedValue = value.replace(ESCAPED_VARIABLE_PATTERN, (_match, body: string) => {
    const token = `\u0000${escaped.length}\u0000`;
    escaped.push(`\${${body}}`);
    return token;
  });

  const replaced = protectedValue.replace(
    VARIABLE_REFERENCE_PATTERN,
    (match, key: string, defaultValue: string | undefined) => {
      const resolved = resolveVariableReference({ defaultValue, key }, variables, issues, context);
      return resolved === undefined ? match : String(resolved);
    },
  );

  return escaped.reduce((result, literal, index) => result.replace(`\u0000${index}\u0000`, literal), replaced);
}

function resolveTargetVariables(
  target: unknown,
  index: number,
  variables: Map<string, VariableValue>,
  issues: ConfigValidationIssue[],
): unknown {
  if (!isPlainObject(target)) return target;
  const targetRecord = target as Record<string, unknown>;
  const idValue: unknown = targetRecord["id"];
  const nameValue: unknown = targetRecord["name"];
  const targetId = isString(idValue) ? idValue : undefined;
  const targetName = isString(nameValue) ? nameValue : targetId;
  return resolveValue(target, `targets[${index}]`, variables, issues, {
    path: `targets[${index}]`,
    targetId,
    targetName,
  });
}

function resolveValue(
  value: unknown,
  path: string,
  variables: Map<string, VariableValue>,
  issues: ConfigValidationIssue[],
  context: VariableResolutionIssueContext,
): unknown {
  if (isString(value)) {
    return replaceStringValue(value, variables, issues, { ...context, path });
  }
  if (Array.isArray(value)) {
    return value.map((item, index) =>
      resolveValue(item, `${path}[${index}]`, variables, issues, { ...context, path: `${path}[${index}]` }),
    );
  }
  if (!isPlainObject(value)) return value;

  const result: Record<string, unknown> = {};
  for (const [key, item] of Object.entries(value)) {
    const itemPath = joinPath(path, key);
    result[key] =
      key === "id" || key === "type"
        ? item
        : resolveValue(item, itemPath, variables, issues, { ...context, path: itemPath });
  }
  return result;
}

function resolveVariableReference(
  reference: VariableReference,
  variables: Map<string, VariableValue>,
  issues: ConfigValidationIssue[],
  context: VariableResolutionIssueContext,
): undefined | VariableValue {
  if (variables.has(reference.key)) {
    return variables.get(reference.key);
  }

  if (Object.prototype.hasOwnProperty.call(process.env, reference.key)) {
    return inferStringValue(process.env[reference.key] ?? "");
  }

  if (reference.defaultValue !== undefined) {
    return inferStringValue(reference.defaultValue);
  }

  issues.push(
    issue(
      "unresolved-variable",
      context.path,
      `引用了未定义的变量 "${reference.key}"，且环境变量中也不存在，未设置默认值`,
      context.targetName,
      context.targetId,
    ),
  );
  return undefined;
}
