import { isError } from "es-toolkit";
import { resolve } from "node:path";

import type { CheckResult, RawTargetConfig } from "../../types";
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
import type {
  CommandTargetConfig,
  RawCommandExpectConfig,
  ResolvedCommandExpectConfig,
  ResolvedCommandTarget,
} from "./types";

import { checkContentExpectations, resolveContentExpectations } from "../../expect/content";
import { errorFailure } from "../../expect/failure";
import { checkValueExpectation, resolveValueExpectation } from "../../expect/value";
import { parseSize } from "../../utils";
import { checkExitCode } from "./expect";
import { commandCheckerSchemas } from "./schema";
import { validateCommandConfig } from "./validate";

const STDOUT_PREVIEW_MAX = 1024;
const STDERR_PREVIEW_MAX = 1024;

export class CommandChecker implements CheckerDefinition<ResolvedCommandTarget> {
  readonly configKey = "cmd";

  readonly schemas = commandCheckerSchemas;

  readonly type = "cmd";

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

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

    let proc: ReturnType<typeof Bun.spawn>;

    try {
      proc = Bun.spawn([t.cmd.exec, ...t.cmd.args], {
        cwd: t.cmd.cwd,
        env: t.cmd.env,
        stderr: "pipe",
        stdin: "ignore",
        stdout: "pipe",
      });
    } catch (error) {
      const durationMs = Math.round(performance.now() - start);
      return {
        detail: null,
        durationMs,
        failure: errorFailure("exitCode", "spawn", 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 },
    );

    let outputResult: { exceeded: boolean; stderr: string; stdout: string };

    try {
      outputResult = await readOutput(
        proc.stdout as ReadableStream<Uint8Array>,
        proc.stderr as ReadableStream<Uint8Array>,
        () => proc.kill(),
        t.cmd.maxOutputBytes,
      );
    } catch {
      const durationMs = Math.round(performance.now() - start);
      return {
        detail: null,
        durationMs,
        failure: errorFailure("exitCode", "execution", "输出读取失败"),
        matched: false,
        observation: null,
        targetId: t.id,
        timestamp,
      };
    }

    await proc.exited;

    const durationMs = Math.round(performance.now() - start);
    const exitCode = proc.exitCode ?? 1;
    const stdoutPreview = truncatePreview(outputResult.stdout, STDOUT_PREVIEW_MAX);
    const stderrPreview = truncatePreview(outputResult.stderr, STDERR_PREVIEW_MAX);
    const observation: Record<string, unknown> = { error: null, exitCode, stderrPreview, stdoutPreview };

    if (outputResult.exceeded) {
      const message = `输出超过限制 ${t.cmd.maxOutputBytes} 字节`;
      observation["error"] = message;
      return {
        detail: null,
        durationMs,
        failure: errorFailure("exitCode", "output", message),
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    if (ctx.signal.aborted) {
      const message = `命令执行超时 (${t.timeoutMs}ms)`;
      observation["error"] = message;
      return {
        detail: null,
        durationMs,
        failure: errorFailure("exitCode", "timeout", message),
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    const exitCodeResult = checkExitCode(exitCode, t.expect?.exitCode ?? [0]);
    if (!exitCodeResult.matched) {
      return {
        detail: null,
        durationMs,
        failure: exitCodeResult.failure,
        matched: false,
        observation,
        targetId: t.id,
        timestamp,
      };
    }

    const durationResult = checkValueExpectation(durationMs, t.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,
      };
    }

    if (t.expect?.stdout && t.expect.stdout.length > 0) {
      const stdoutResult = checkContentExpectations(outputResult.stdout, t.expect.stdout, {
        path: "stdout",
        phase: "stdout",
      });
      if (!stdoutResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: stdoutResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

    if (t.expect?.stderr && t.expect.stderr.length > 0) {
      const stderrResult = checkContentExpectations(outputResult.stderr, t.expect.stderr, {
        path: "stderr",
        phase: "stderr",
      });
      if (!stderrResult.matched) {
        return {
          detail: null,
          durationMs,
          failure: stderrResult.failure,
          matched: false,
          observation,
          targetId: t.id,
          timestamp,
        };
      }
    }

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

  resolve(target: RawTargetConfig, context: ResolveContext): ResolvedCommandTarget {
    const t = target as RawTargetConfig & { cmd: CommandTargetConfig; type: "cmd" };
    const cmdDefaults = context.defaults["cmd"] as undefined | { cwd?: string; maxOutputBytes?: string };

    const cwd = t.cmd.cwd ?? cmdDefaults?.cwd ?? ".";
    const resolvedCwd = resolve(context.configDir, cwd);

    const maxOutputBytes = parseSize(t.cmd.maxOutputBytes ?? cmdDefaults?.maxOutputBytes ?? "100MB");

    const env = { ...process.env, ...(t.cmd.env ?? {}) } as Record<string, string>;

    const rawExpect = target.expect as RawCommandExpectConfig | undefined;
    const resolvedExpect: ResolvedCommandExpectConfig = rawExpect
      ? {
          durationMs: resolveValueExpectation(rawExpect.durationMs),
          exitCode: rawExpect.exitCode ?? [0],
          stderr: resolveContentExpectations(rawExpect.stderr),
          stdout: resolveContentExpectations(rawExpect.stdout),
        }
      : { exitCode: [0] };

    return {
      cmd: {
        args: t.cmd.args ?? [],
        cwd: resolvedCwd,
        env,
        exec: t.cmd.exec,
        maxOutputBytes,
      },
      description: null,
      expect: resolvedExpect,
      group: target.group ?? "default",
      id: t.id,
      intervalMs: context.defaultIntervalMs,
      name: t.name ?? null,
      rawExpect,
      timeoutMs: context.defaultTimeoutMs,
      type: "cmd",
    } satisfies ResolvedCommandTarget;
  }

  serialize(t: ResolvedCommandTarget): { config: string; target: string } {
    const parts = [t.cmd.exec, ...t.cmd.args];
    return {
      config: JSON.stringify({
        args: t.cmd.args,
        cwd: t.cmd.cwd,
        env: t.cmd.env,
        exec: t.cmd.exec,
        maxOutputBytes: t.cmd.maxOutputBytes,
      }),
      target: `exec ${parts.join(" ")}`,
    };
  }

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

async function readOutput(
  stdout: ReadableStream<Uint8Array>,
  stderr: ReadableStream<Uint8Array>,
  kill: () => void,
  maxBytes: number,
): Promise<{ exceeded: boolean; stderr: string; stdout: string }> {
  let totalBytes = 0;
  let exceeded = false;
  let killed = false;

  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;
        totalBytes += value.byteLength;
        text += decoder.decode(value, { stream: true });
        if (totalBytes > maxBytes && !killed) {
          exceeded = true;
          killed = true;
          try {
            kill();
          } catch {
            /* best-effort kill */
          }
        }
      }
    } catch {
      /* stream already closed */
    } finally {
      try {
        reader.releaseLock();
      } catch {
        /* already released */
      }
    }

    return text;
  }

  const [out, err] = await Promise.all([readStream(stdout), readStream(stderr)]);

  return { exceeded, stderr: err, stdout: out };
}

function truncatePreview(text: string, maxLen: number): string {
  if (text.length <= maxLen) return text;
  return text.slice(0, maxLen);
}
