import { isError } from "es-toolkit";

import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type {
  PingStats,
  PingTargetConfig,
  RawIcmpExpectConfig,
  ResolvedIcmpExpectConfig,
  ResolvedPingTarget,
} from "./types";

import { errorFailure } from "../../expect/failure";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { buildPingCommand } from "./command";
import { checkAlive, checkAvgLatency, checkMaxLatency, checkPacketLoss } from "./expect";
import { parsePingOutput } from "./parse";
import { icmpCheckerSchemas } from "./schema";
import { validatePingConfig } from "./validate";

const DEFAULT_COUNT = 3;
const DEFAULT_PACKET_SIZE = 56;

export class IcmpChecker implements CheckerDefinition<ResolvedPingTarget> {
  readonly configKey = "icmp";

  readonly platform: NodeJS.Platform;

  readonly schemas = icmpCheckerSchemas;

  readonly type = "icmp";

  constructor(platform: NodeJS.Platform = process.platform) {
    this.platform = platform;
  }

  buildDetail(observation: Record<string, unknown>): null | string {
    const alive = observation["alive"];
    const transmitted = observation["transmitted"];
    const received = observation["received"];

    if (alive !== true) {
      const rx = typeof received === "number" ? received : 0;
      const tx = typeof transmitted === "number" ? transmitted : 0;
      return `unreachable (${rx}/${tx} received)`;
    }

    const avg = observation["avgLatencyMs"];
    const loss = observation["packetLoss"];
    const avgStr = typeof avg === "number" ? formatNumber(avg) : "n/a";
    const lossStr = typeof loss === "number" ? formatNumber(loss) : "0";
    const rx = typeof received === "number" ? received : 0;
    const tx = typeof transmitted === "number" ? transmitted : 0;
    let detail = `alive, avg ${avgStr}ms, loss ${lossStr}% (${rx}/${tx})`;

    if (typeof loss === "number" && loss > 0) {
      const max = observation["maxLatencyMs"];
      if (typeof max === "number") {
        detail = `${detail}, max ${formatNumber(max)}ms`;
      }
    }

    return detail;
  }

  async execute(t: ResolvedPingTarget, ctx: CheckerContext): Promise<CheckResult> {
    const timestamp = new Date().toISOString();
    const start = performance.now();
    let proc: ReturnType<typeof Bun.spawn>;

    try {
      proc = Bun.spawn(buildPingCommand(t), {
        stderr: "pipe",
        stdin: "ignore",
        stdout: "pipe",
      });
    } catch (error) {
      const durationMs = Math.round(performance.now() - start);
      return {
        detail: null,
        durationMs,
        failure: errorFailure("icmp", "spawn", `icmp 命令不可用: ${isError(error) ? error.message : String(error)}`),
        matched: false,
        observation: null,
        targetId: t.id,
        timestamp,
      };
    }

    ctx.signal.addEventListener(
      "abort",
      () => {
        try {
          proc.kill();
        } catch {
          /* best-effort kill */
        }
      },
      { once: true },
    );

    const stdout = await readStream(proc.stdout as ReadableStream<Uint8Array>);
    await proc.exited;

    const durationMs = Math.round(performance.now() - start);
    if (ctx.signal.aborted) {
      return {
        detail: null,
        durationMs,
        failure: errorFailure("icmp", "timeout", `icmp 执行超时 (${t.timeoutMs}ms)`),
        matched: false,
        observation: null,
        targetId: t.id,
        timestamp,
      };
    }

    const stats = parsePingOutput(stdout, this.platform);
    if (!stats) {
      return {
        detail: null,
        durationMs,
        failure: errorFailure("icmp", "parse", "无法解析 icmp 输出"),
        matched: false,
        observation: {
          alive: false,
          avgLatencyMs: null,
          error: "parse failed",
          maxLatencyMs: null,
          minLatencyMs: null,
          packetLoss: 100,
          received: 0,
          transmitted: 0,
        },
        targetId: t.id,
        timestamp,
      };
    }

    const result = checkStats(stats, t.expect, durationMs);
    const observation: Record<string, unknown> = {
      alive: stats.alive,
      avgLatencyMs: stats.avgLatencyMs,
      error: null,
      maxLatencyMs: stats.maxLatencyMs,
      minLatencyMs: stats.minLatencyMs,
      packetLoss: stats.packetLoss,
      received: stats.received,
      transmitted: stats.transmitted,
    };
    return {
      detail: null,
      durationMs,
      failure: result.failure,
      matched: result.matched,
      observation,
      targetId: t.id,
      timestamp,
    };
  }

  resolve(target: RawTargetConfig, context: ResolveContext): ResolvedPingTarget {
    const t = target as RawTargetConfig & { icmp: PingTargetConfig; type: "icmp" };

    const rawExpect = target.expect as RawIcmpExpectConfig | undefined;
    const resolvedExpect: ResolvedIcmpExpectConfig = rawExpect
      ? {
          alive: rawExpect.alive ?? true,
          avgLatencyMs: resolveValueExpectation(rawExpect.avgLatencyMs),
          durationMs: resolveValueExpectation(rawExpect.durationMs),
          maxLatencyMs: resolveValueExpectation(rawExpect.maxLatencyMs),
          packetLossPercent: resolveValueExpectation(rawExpect.packetLossPercent),
        }
      : { alive: true };

    return {
      description: null,
      expect: resolvedExpect,
      group: target.group ?? "default",
      icmp: {
        count: t.icmp.count ?? DEFAULT_COUNT,
        host: t.icmp.host,
        packetSize: t.icmp.packetSize ?? DEFAULT_PACKET_SIZE,
      },
      id: t.id,
      intervalMs: context.defaultIntervalMs,
      name: t.name ?? null,
      rawExpect,
      timeoutMs: context.defaultTimeoutMs,
      type: "icmp",
    } satisfies ResolvedPingTarget;
  }

  serialize(t: ResolvedPingTarget): { config: string; target: string } {
    return {
      config: JSON.stringify(t.icmp),
      target: `icmp ${t.icmp.host}`,
    };
  }

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

function checkStats(stats: PingStats, expect: ResolvedIcmpExpectConfig | undefined, durationMs: number) {
  const aliveResult = checkAlive(stats.alive, expect?.alive ?? true);
  if (!aliveResult.matched) return aliveResult;
  const packetLossResult = checkPacketLoss(stats.packetLoss, expect?.packetLossPercent);
  if (!packetLossResult.matched) return packetLossResult;
  const avgLatencyResult = checkAvgLatency(stats.avgLatencyMs, expect?.avgLatencyMs);
  if (!avgLatencyResult.matched) return avgLatencyResult;
  const maxLatencyResult = checkMaxLatency(stats.maxLatencyMs, expect?.maxLatencyMs);
  if (!maxLatencyResult.matched) return maxLatencyResult;
  return checkValueExpectation(durationMs, expect?.durationMs, {
    message: "durationMs mismatch",
    path: "durationMs",
    phase: "duration",
  });
}

function formatNumber(value: number): string {
  return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(3)));
}

async function readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
  const reader = stream.getReader();
  const decoder = new TextDecoder();
  let text = "";
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      text += decoder.decode(value, { stream: true });
    }
    text += decoder.decode();
  } catch {
    /* stream already closed */
  } finally {
    try {
      reader.releaseLock();
    } catch {
      /* already released */
    }
  }
  return text;
}
