import { isError } from "es-toolkit";

import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type {
  DnsServerConfig,
  DnsSystemConfig,
  ResolvedDnsServerExpectConfig,
  ResolvedDnsSystemExpectConfig,
  ResolvedDnsTarget,
} from "./types";

import { errorFailure, mismatchFailure } from "../../expect/failure";
import { checkValueExpectation } from "../../expect/value";
import { parseSize } from "../../utils";
import { buildQuery, rcodeName, rrtTypeByName, rrtTypeName } from "./codec";
import {
  checkAnswerCount,
  checkDnsValues,
  checkFlag,
  checkRcode,
  checkResponded,
  checkResult,
  checkTtlMax,
  checkTtlMin,
  checkValueCount,
} from "./expect";
import { normalizeTargetExpect } from "./normalize";
import { dnsCheckerSchemas } from "./schema";
import { queryDns } from "./transport";
import { validateDnsConfig } from "./validate";

const DEFAULT_MAX_RESPONSE_BYTES = 4096;
const DEFAULT_PORT = 53;
const DEFAULT_RECORD_TYPE = "A";
const DEFAULT_PROTOCOL = "udp";
const DEFAULT_TCP_FALLBACK = true;
const DEFAULT_RECURSION_DESIRED = true;
const DEFAULT_FAMILY = "any";

interface LookupAddress {
  address: string;
  family: number;
}
type LookupOutcome = { addresses: string[]; ok: true } | { error: string; ok: false };

export class DnsChecker implements CheckerDefinition<ResolvedDnsTarget> {
  readonly configKey = "dns";
  readonly schemas = dnsCheckerSchemas;
  readonly type = "dns";

  buildDetail(observation: Record<string, unknown>): null | string {
    const resolver = observation["resolver"];
    const durationMs = observation["durationMs"];
    const duration = typeof durationMs === "number" ? `${durationMs}ms` : "?ms";

    if (resolver === "system") {
      return buildSystemDetail(observation, duration);
    }

    return buildServerDetail(observation, duration);
  }

  async execute(t: ResolvedDnsTarget, ctx: CheckerContext): Promise<CheckResult> {
    const timestamp = new Date().toISOString();
    const start = performance.now();

    try {
      if (t.dns.resolver === "system") {
        return await this.executeSystem(t, ctx, timestamp, start);
      }
      return await this.executeServer(t, ctx, timestamp, start);
    } catch (error) {
      const durationMs = Math.round(performance.now() - start);
      return {
        detail: null,
        durationMs,
        failure: errorFailure("query", "query", isError(error) ? error.message : String(error)),
        matched: false,
        observation: null,
        targetId: t.id,
        timestamp,
      };
    }
  }

  normalize(target: RawTargetConfig): RawTargetConfig {
    return normalizeTargetExpect(target);
  }

  resolve(target: RawTargetConfig, context: ResolveContext): ResolvedDnsTarget {
    const dns = target["dns"] as DnsServerConfig | DnsSystemConfig;

    if (dns.resolver === "system") {
      return this.resolveSystem(target, dns, context);
    }

    return this.resolveServer(target, dns, context);
  }

  serialize(t: ResolvedDnsTarget): { config: string; target: string } {
    if (t.dns.resolver === "system") {
      return {
        config: JSON.stringify(t.dns),
        target: `dns system ${t.dns.name}`,
      };
    }
    return {
      config: JSON.stringify(t.dns),
      target: `dns ${t.dns.server}:${t.dns.port} ${t.dns.name}/${t.dns.recordType}`,
    };
  }

  validate(input: CheckerValidationInput) {
    return validateDnsConfig(input);
  }

  private async executeServer(
    t: ResolvedDnsTarget,
    ctx: CheckerContext,
    timestamp: string,
    start: number,
  ): Promise<CheckResult> {
    const dns = t.dns as {
      maxResponseBytes: number;
      name: string;
      port: number;
      protocol: "tcp" | "udp";
      recordType: string;
      recursionDesired: boolean;
      resolver: "server";
      server: string;
      tcpFallback: boolean;
    };
    const expect = t.expect as ResolvedDnsServerExpectConfig | undefined;

    const qtype = rrtTypeByName(dns.recordType);
    const query = buildQuery(dns.name, qtype, dns.recursionDesired);

    const queryResult = await queryDns(dns.server, dns.port, query, {
      maxResponseBytes: dns.maxResponseBytes,
      protocol: dns.protocol,
      signal: ctx.signal,
      tcpFallback: dns.tcpFallback,
    });

    const durationMs = Math.round(performance.now() - start);

    if (!queryResult.ok) {
      const observation: Record<string, unknown> = {
        durationMs,
        error: queryResult.error,
        name: dns.name,
        port: dns.port,
        protocol: dns.protocol,
        protocolUsed: dns.protocol,
        recordType: dns.recordType,
        recursionDesired: dns.recursionDesired,
        resolver: "server",
        responded: false,
        server: dns.server,
        tcpFallback: dns.tcpFallback,
      };

      const expectedResponded = expect?.responded ?? true;
      if (expectedResponded) {
        return {
          detail: null,
          durationMs,
          failure: errorFailure("query", "query", queryResult.error),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }

      const durationResult = checkValueExpectation(durationMs, expect?.durationMs, {
        message: "durationMs mismatch",
        path: "durationMs",
        phase: "duration",
      });
      if (!durationResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: durationResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }

      return {
        detail: null,
        durationMs,
        failure: null,
        matched: true,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    const response = queryResult.response;
    const rcode = rcodeName(response.header.rcode);
    const protocolUsed = queryResult.protocolUsed;

    const allAnswers = response.answers;
    const targetAnswers = allAnswers.filter((a) => a.type === qtype);
    const values = targetAnswers.map((a) => a.value);
    const valueCount = values.length;
    const answerCount = allAnswers.length;

    let ttlMin: null | number = null;
    let ttlMax: null | number = null;
    if (allAnswers.length > 0) {
      const ttls = allAnswers.map((a) => a.ttl);
      ttlMin = Math.min(...ttls);
      ttlMax = Math.max(...ttls);
    }

    const cnameChain: string[] = [];
    const finalValues: string[] = [];

    if (dns.recordType === "A" || dns.recordType === "AAAA") {
      const targetType = dns.recordType === "A" ? 1 : 28;
      const seen = new Set<string>();
      for (const ans of allAnswers) {
        if (ans.type === targetType) {
          if (!seen.has(ans.value)) {
            finalValues.push(ans.value);
            seen.add(ans.value);
          }
        } else if (ans.type === 5) {
          cnameChain.push(ans.value);
        }
      }
    } else {
      for (const v of values) {
        if (!finalValues.includes(v)) {
          finalValues.push(v);
        }
      }
    }

    const effectiveValueCount = dns.recordType === "A" || dns.recordType === "AAAA" ? finalValues.length : valueCount;
    const effectiveValues = dns.recordType === "A" || dns.recordType === "AAAA" ? finalValues : values;

    const observation: Record<string, unknown> = {
      additionalCount: response.header.additionalCount,
      answerCount,
      answers: allAnswers.map((a) => ({
        class: a.class === 1 ? "IN" : a.class,
        data: a.data,
        name: a.name,
        ttl: a.ttl,
        type: rrtTypeName(a.type),
        value: a.value,
      })),
      authorityCount: response.header.authorityCount,
      cnameChain,
      durationMs,
      error: null,
      flags: {
        authenticatedData: response.header.flags.authenticatedData,
        authoritative: response.header.flags.authoritative,
        recursionAvailable: response.header.flags.recursionAvailable,
        recursionDesired: response.header.flags.recursionDesired,
        truncated: response.header.flags.truncated,
      },
      name: dns.name,
      port: dns.port,
      protocol: dns.protocol,
      protocolUsed,
      rcode,
      recordType: dns.recordType,
      recursionDesired: dns.recursionDesired,
      resolver: "server",
      responded: true,
      server: dns.server,
      tcpFallback: dns.tcpFallback,
      ttlMax,
      ttlMin,
      valueCount: effectiveValueCount,
      values: effectiveValues,
    };

    if (!expect) {
      const defaultRcodeOk = rcode === "NOERROR";
      const defaultCountOk = effectiveValueCount > 0;
      if (!defaultRcodeOk) {
        return {
          detail: null,
          durationMs,
          failure: mismatchFailure("rcode", "rcode", "NOERROR", rcode, `DNS 响应码: ${rcode}`),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
      if (!defaultCountOk) {
        return {
          detail: null,
          durationMs,
          failure: mismatchFailure("valueCount", "valueCount", "> 0", 0, "DNS 响应成功但无目标记录"),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
      return {
        detail: null,
        durationMs,
        failure: null,
        matched: true,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    const expectedResponded = expect.responded;
    const respondedResult = checkResponded(true, expectedResponded);
    if (!respondedResult.matched) {
      return {
        detail: null,
        durationMs,
        failure: respondedResult.failure,
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    if (expect.rcode) {
      const rcodeResult = checkRcode(rcode, expect.rcode);
      if (!rcodeResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: rcodeResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    const hasExplicitNonNoerrorRcode = expect.rcode && !expect.rcode.includes("NOERROR");

    if (expect.values) {
      const valuesResult = checkDnsValues(effectiveValues, expect.values);
      if (!valuesResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: valuesResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    if (expect.valueCount) {
      const countResult = checkValueCount(effectiveValueCount, expect.valueCount);
      if (!countResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: countResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    } else if (!hasExplicitNonNoerrorRcode && !expect.values) {
      if (effectiveValueCount === 0) {
        return {
          detail: null,
          durationMs,
          failure: mismatchFailure("valueCount", "valueCount", "> 0", 0, "DNS 响应成功但无目标记录"),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    if (expect.answerCount) {
      const countResult = checkAnswerCount(answerCount, expect.answerCount);
      if (!countResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: countResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    if (expect.ttlMin !== undefined) {
      if (ttlMin === null) {
        return {
          detail: null,
          durationMs,
          failure: mismatchFailure("ttlMin", "ttlMin", "可用 TTL", null, "响应中没有可用于 ttlMin 的 answer"),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
      const ttlResult = checkTtlMin(ttlMin, expect.ttlMin);
      if (!ttlResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: ttlResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    if (expect.ttlMax !== undefined) {
      if (ttlMax === null) {
        return {
          detail: null,
          durationMs,
          failure: mismatchFailure("ttlMax", "ttlMax", "可用 TTL", null, "响应中没有可用于 ttlMax 的 answer"),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
      const ttlResult = checkTtlMax(ttlMax, expect.ttlMax);
      if (!ttlResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: ttlResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    const authoritativeResult = checkFlag(response.header.flags.authoritative, expect.authoritative, "authoritative");
    if (authoritativeResult && !authoritativeResult.matched) {
      return {
        detail: null,
        durationMs,
        failure: authoritativeResult.failure,
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    const raResult = checkFlag(
      response.header.flags.recursionAvailable,
      expect.recursionAvailable,
      "recursionAvailable",
    );
    if (raResult && !raResult.matched) {
      return {
        detail: null,
        durationMs,
        failure: raResult.failure,
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    const tcResult = checkFlag(response.header.flags.truncated, expect.truncated, "truncated");
    if (tcResult && !tcResult.matched) {
      return {
        detail: null,
        durationMs,
        failure: tcResult.failure,
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    const adResult = checkFlag(response.header.flags.authenticatedData, expect.authenticatedData, "authenticatedData");
    if (adResult && !adResult.matched) {
      return {
        detail: null,
        durationMs,
        failure: adResult.failure,
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    if (expect.result) {
      const resultExpectation = checkResult(observation, expect.result);
      if (!resultExpectation.matched) {
        return {
          detail: null,
          durationMs,
          failure: resultExpectation.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    const durationResult = checkValueExpectation(durationMs, expect.durationMs, {
      message: "durationMs mismatch",
      path: "durationMs",
      phase: "duration",
    });
    if (!durationResult.matched) {
      return {
        detail: null,
        durationMs,
        failure: durationResult.failure,
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    return {
      detail: null,
      durationMs,
      failure: null,
      matched: true,
      observation,
      targetId: t.id,
      timestamp,
    };
  }

  private async executeSystem(
    t: ResolvedDnsTarget,
    ctx: CheckerContext,
    timestamp: string,
    start: number,
  ): Promise<CheckResult> {
    const dns = t.dns as { family: string; name: string; resolver: "system" };
    const expect = t.expect;

    try {
      const familyOption = dns.family === "ipv4" ? 4 : dns.family === "ipv6" ? 6 : 0;

      const result = await lookupWithAbort(dns.name, familyOption, ctx.signal);

      const durationMs = Math.round(performance.now() - start);

      if (!result.ok) {
        const observation: Record<string, unknown> = {
          durationMs,
          error: result.error,
          family: dns.family,
          name: dns.name,
          resolver: "system",
          valueCount: 0,
          values: [],
        };

        const defaultExpect = !expect;
        if (defaultExpect) {
          return {
            detail: null,
            durationMs,
            failure: errorFailure("resolve", "resolve", result.error),
            matched: false,
            observation,
            targetId: t.id,
            timestamp,
          };
        }

        const durationResult = checkValueExpectation(durationMs, expect?.durationMs, {
          message: "durationMs mismatch",
          path: "durationMs",
          phase: "duration",
        });
        if (!durationResult.matched) {
          return {
            detail: null,
            durationMs,
            failure: durationResult.failure,
            matched: false,
            observation,
            targetId: t.id,
            timestamp,
          };
        }

        return {
          detail: null,
          durationMs,
          failure: errorFailure("resolve", "resolve", result.error),
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }

      const values = result.addresses;
      const valueCount = values.length;

      const observation: Record<string, unknown> = {
        durationMs,
        error: null,
        family: dns.family,
        name: dns.name,
        resolver: "system",
        valueCount,
        values,
      };

      if (!expect) {
        const defaultMatched = valueCount > 0;
        if (!defaultMatched) {
          return {
            detail: null,
            durationMs,
            failure: mismatchFailure("valueCount", "valueCount", "> 0", 0, "解析成功但未返回任何地址"),
            matched: false,
            observation,
            targetId: t.id,
            timestamp,
          };
        }
        return {
          detail: null,
          durationMs,
          failure: null,
          matched: true,
          observation,
          targetId: t.id,
          timestamp,
        };
      }

      if (expect.values) {
        const valuesResult = checkDnsValues(values, expect.values);
        if (!valuesResult.matched) {
          return {
            detail: null,
            durationMs,
            failure: valuesResult.failure,
            matched: false,
            observation,
            targetId: t.id,
            timestamp,
          };
        }
      }

      if (expect.valueCount) {
        const countResult = checkValueCount(valueCount, expect.valueCount);
        if (!countResult.matched) {
          return {
            detail: null,
            durationMs,
            failure: countResult.failure,
            matched: false,
            observation,
            targetId: t.id,
            timestamp,
          };
        }
      }

      const durationResult = checkValueExpectation(durationMs, expect.durationMs, {
        message: "durationMs mismatch",
        path: "durationMs",
        phase: "duration",
      });
      if (!durationResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: durationResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }

      return {
        detail: null,
        durationMs,
        failure: null,
        matched: true,
        observation,
        targetId: t.id,
        timestamp,
      };
    } catch (error) {
      const durationMs = Math.round(performance.now() - start);
      return {
        detail: null,
        durationMs,
        failure: errorFailure("resolve", "resolve", isError(error) ? error.message : String(error)),
        matched: false,
        observation: null,
        targetId: t.id,
        timestamp,
      };
    }
  }

  private resolveServer(t: RawTargetConfig, dns: DnsServerConfig, context: ResolveContext): ResolvedDnsTarget {
    const expect = t.expect as ResolvedDnsServerExpectConfig | undefined;
    const resolvedExpect: ResolvedDnsServerExpectConfig | undefined = expect
      ? {
          ...expect,
          responded: expect.responded ?? true,
        }
      : undefined;

    return {
      description: null,
      dns: {
        maxResponseBytes: parseSize(dns.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES),
        name: dns.name,
        port: dns.port ?? DEFAULT_PORT,
        protocol: dns.protocol ?? DEFAULT_PROTOCOL,
        recordType: dns.recordType ?? DEFAULT_RECORD_TYPE,
        recursionDesired: dns.recursionDesired ?? DEFAULT_RECURSION_DESIRED,
        resolver: "server",
        server: dns.server,
        tcpFallback: dns.tcpFallback ?? DEFAULT_TCP_FALLBACK,
      },
      expect: resolvedExpect,
      group: t.group ?? "default",
      id: t.id,
      intervalMs: context.defaultIntervalMs,
      name: t.name ?? null,
      timeoutMs: context.defaultTimeoutMs,
      type: "dns",
    } satisfies ResolvedDnsTarget;
  }

  private resolveSystem(t: RawTargetConfig, dns: DnsSystemConfig, context: ResolveContext): ResolvedDnsTarget {
    const expect = t.expect as ResolvedDnsSystemExpectConfig | undefined;
    const resolvedExpect: ResolvedDnsSystemExpectConfig | undefined = expect ? { ...expect } : undefined;

    return {
      description: null,
      dns: {
        family: dns.family ?? DEFAULT_FAMILY,
        name: dns.name,
        resolver: "system",
      },
      expect: resolvedExpect,
      group: t.group ?? "default",
      id: t.id,
      intervalMs: context.defaultIntervalMs,
      name: t.name ?? null,
      timeoutMs: context.defaultTimeoutMs,
      type: "dns",
    } satisfies ResolvedDnsTarget;
  }
}

function buildServerDetail(observation: Record<string, unknown>, duration: string): null | string {
  const responded = observation["responded"];
  const error = observation["error"];
  const rcode = observation["rcode"];
  const protocolUsed = observation["protocolUsed"];
  const cnameChain = observation["cnameChain"];

  if (responded !== true) {
    if (typeof error === "string") {
      return `查询失败: ${error} (${duration})`;
    }
    return `未收到响应 (${duration})`;
  }

  const rcodeStr = typeof rcode === "string" ? rcode : "UNKNOWN";
  const valueCount = observation["valueCount"];
  const count = typeof valueCount === "number" ? valueCount : 0;
  const values = observation["values"];
  const addrs = Array.isArray(values) ? values : [];
  const protoStr = typeof protocolUsed === "string" ? protocolUsed.toUpperCase() : "?";

  const parts: string[] = [rcodeStr];

  if (Array.isArray(cnameChain) && cnameChain.length > 0) {
    parts.push(`CNAME: ${cnameChain.join(" → ")}`);
  }

  if (count > 0) {
    const preview = addrs.slice(0, 3).join(", ");
    const suffix = count > 3 ? ` 等 ${count} 条` : "";
    parts.push(`${preview}${suffix}`);
  }

  parts.push(`${protoStr} ${duration}`);

  return parts.join(", ");
}

function buildSystemDetail(observation: Record<string, unknown>, duration: string): null | string {
  const error = observation["error"];
  const valueCount = observation["valueCount"];
  const values = observation["values"];

  if (typeof error === "string") {
    return `解析失败: ${error} (${duration})`;
  }

  const count = typeof valueCount === "number" ? valueCount : 0;
  const addrs = Array.isArray(values) ? values : [];
  if (count === 0) {
    return `解析成功但无结果 (${duration})`;
  }
  const preview = addrs.slice(0, 3).join(", ");
  const suffix = count > 3 ? ` 等 ${count} 条` : "";
  return `${preview}${suffix} (${duration})`;
}

async function lookupWithAbort(hostname: string, family: number, signal: AbortSignal): Promise<LookupOutcome> {
  if (signal.aborted) {
    return { error: "探测已取消", ok: false };
  }

  return new Promise<LookupOutcome>((resolve) => {
    let settled = false;

    const onAbort = () => {
      if (settled) return;
      settled = true;
      resolve({ error: "探测超时", ok: false });
    };
    signal.addEventListener("abort", onAbort, { once: true });

    const onResult = (err: NodeJS.ErrnoException | null, address: LookupAddress[] | string | string[]) => {
      if (settled) return;
      settled = true;
      signal.removeEventListener("abort", onAbort);

      if (err) {
        resolve({ error: err.message, ok: false });
        return;
      }

      const addresses = Array.isArray(address)
        ? address.map((item) => (typeof item === "string" ? item : item.address))
        : [address];
      resolve({ addresses, ok: true });
    };

    try {
      import("node:dns")
        .then((dns) => {
          if (family === 0) {
            dns.lookup(hostname, { all: true }, (err, address) => onResult(err, address));
          } else {
            dns.lookup(hostname, { all: true, family }, (err, address) => onResult(err, address));
          }
        })
        .catch((e) => {
          if (!settled) {
            settled = true;
            signal.removeEventListener("abort", onAbort);
            resolve({ error: e instanceof Error ? e.message : String(e), ok: false });
          }
        });
    } catch (e) {
      if (!settled) {
        settled = true;
        signal.removeEventListener("abort", onAbort);
        resolve({ error: e instanceof Error ? e.message : String(e), ok: false });
      }
    }
  });
}
