import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { rootPath } from "./config";

export type EgressProxySourceType = "subscription-url" | "master-shadowsocks";
export type EgressProxyPreferredOutbound = "vless-reality" | "hysteria2";

export interface MasterShadowsocksSourceSpec {
  serverHost: string;
  serverPort: number;
  method: string;
}

export interface SharedEgressProxySourceSpec {
  id: string;
  configRef: string;
  sourceType: EgressProxySourceType;
  sourceRef: string;
  sourceKey: string;
  preferredOutbound: EgressProxyPreferredOutbound | null;
  masterShadowsocks: MasterShadowsocksSourceSpec | null;
  healthProbeUrl: string;
  fingerprint: string;
  valuesPrinted: false;
}

export function resolveEgressProxySourceRef(ref: string, label: string): SharedEgressProxySourceSpec {
  const { file, path } = parseConfigRef(ref, label);
  if (!path.startsWith("sources.")) throw new Error(`${label} must reference sources.<id>`);
  const sourceId = path.slice("sources.".length);
  if (!/^[A-Za-z0-9._-]+$/u.test(sourceId)) throw new Error(`${label} has an unsupported source id`);
  const parsed = Bun.YAML.parse(readFileSync(rootPath(file), "utf8")) as unknown;
  const root = record(parsed, file);
  if (root.kind !== "platform-infra-egress-proxy-sources") throw new Error(`${file}.kind must be platform-infra-egress-proxy-sources`);
  const sources = record(root.sources, `${file}.sources`);
  const raw = record(sources[sourceId], `${file}#sources.${sourceId}`);
  return parseSharedEgressProxySource(sourceId, ref, raw, `${file}#sources.${sourceId}`);
}

function parseSharedEgressProxySource(id: string, configRef: string, raw: Record<string, unknown>, label: string): SharedEgressProxySourceSpec {
  const sourceType = enumField(raw, "sourceType", label, ["subscription-url", "master-shadowsocks"] as const);
  const preferredOutbound = raw.preferredOutbound === undefined
    ? null
    : enumField(raw, "preferredOutbound", label, ["vless-reality", "hysteria2"] as const);
  const masterShadowsocks = raw.masterShadowsocks === undefined
    ? null
    : masterShadowsocksSpec(record(raw.masterShadowsocks, `${label}.masterShadowsocks`), `${label}.masterShadowsocks`);
  const spec: SharedEgressProxySourceSpec = {
    id,
    configRef,
    sourceType,
    sourceRef: sourceRefField(raw, "sourceRef", label),
    sourceKey: envKeyField(raw, "sourceKey", label),
    preferredOutbound,
    masterShadowsocks,
    healthProbeUrl: urlField(raw, "healthProbeUrl", label),
    fingerprint: "",
    valuesPrinted: false,
  };
  validateSharedEgressProxySource(spec, label);
  return {
    ...spec,
    fingerprint: `sha256:${createHash("sha256").update(JSON.stringify({
      sourceType: spec.sourceType,
      sourceRef: spec.sourceRef,
      sourceKey: spec.sourceKey,
      preferredOutbound: spec.preferredOutbound,
      masterShadowsocks: spec.masterShadowsocks,
      healthProbeUrl: spec.healthProbeUrl,
    })).digest("hex").slice(0, 16)}`,
  };
}

function validateSharedEgressProxySource(spec: SharedEgressProxySourceSpec, label: string): void {
  if (spec.sourceType === "subscription-url" && spec.preferredOutbound === null) {
    throw new Error(`${label}.preferredOutbound is required for sourceType=subscription-url`);
  }
  if (spec.sourceType === "master-shadowsocks") {
    if (spec.masterShadowsocks === null) throw new Error(`${label}.masterShadowsocks is required for sourceType=master-shadowsocks`);
    if (spec.preferredOutbound !== null) throw new Error(`${label}.preferredOutbound must be omitted for sourceType=master-shadowsocks`);
  }
}

function masterShadowsocksSpec(raw: Record<string, unknown>, label: string): MasterShadowsocksSourceSpec {
  const serverHost = stringField(raw, "serverHost", label);
  if (!/^[A-Za-z0-9._:-]+$/u.test(serverHost)) throw new Error(`${label}.serverHost has an unsupported format`);
  const serverPort = integerField(raw, "serverPort", label);
  if (serverPort < 1 || serverPort > 65535) throw new Error(`${label}.serverPort must be a TCP port`);
  const method = stringField(raw, "method", label);
  if (!/^[A-Za-z0-9-]+$/u.test(method)) throw new Error(`${label}.method has an unsupported format`);
  return { serverHost, serverPort, method };
}

function parseConfigRef(ref: string, label: string): { file: string; path: string } {
  const [file, path, extra] = ref.split("#");
  if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
    throw new Error(`${label} must use path/to/file.yaml#sources.<id> syntax`);
  }
  if (file.startsWith("/") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
    throw new Error(`${label} must reference a repo-relative config/*.yaml file without ..`);
  }
  if (!/^[A-Za-z0-9_.-]+$/u.test(path)) throw new Error(`${label} has an unsupported YAML path`);
  return { file, path };
}

function record(value: unknown, label: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} must be a YAML object`);
  return value as Record<string, unknown>;
}

function stringField(obj: Record<string, unknown>, key: string, label: string): string {
  const value = obj[key];
  if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${label}.${key} must be a non-empty string`);
  return value.trim();
}

function integerField(obj: Record<string, unknown>, key: string, label: string): number {
  const value = obj[key];
  if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${label}.${key} must be an integer`);
  return value;
}

function enumField<const T extends readonly string[]>(obj: Record<string, unknown>, key: string, label: string, values: T): T[number] {
  const value = stringField(obj, key, label);
  if (!(values as readonly string[]).includes(value)) throw new Error(`${label}.${key} must be one of ${values.join(", ")}`);
  return value as T[number];
}

function sourceRefField(obj: Record<string, unknown>, key: string, label: string): string {
  const value = stringField(obj, key, label);
  if (value.startsWith("/") || value.includes("..") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${label}.${key} has an unsupported sourceRef format`);
  return value;
}

function envKeyField(obj: Record<string, unknown>, key: string, label: string): string {
  const value = stringField(obj, key, label);
  if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${label}.${key} must be an env key`);
  return value;
}

function urlField(obj: Record<string, unknown>, key: string, label: string): string {
  const value = stringField(obj, key, label);
  const url = new URL(value);
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${label}.${key} must use http:// or https://`);
  if (url.username || url.password || url.search || url.hash) throw new Error(`${label}.${key} must not include credentials, query, or hash`);
  return value;
}
