import { groupBy, isError, Semaphore } from "es-toolkit";

import type { Logger } from "../logger";
import type { ProbeStore } from "./store";
import type { CheckResult, ResolvedTargetBase } from "./types";

import { createNoopLogger } from "../logger";
import { errorFailure } from "./expect/failure";
import { checkerRegistry } from "./runner";

const PRUNE_INTERVAL_MS = 3600000;

export class ProbeEngine {
  private lastMatched = new Map<string, boolean>();
  private logger: Logger;
  private retentionMs: number;
  private semaphore: Semaphore;
  private store: ProbeStore;
  private targetIds = new Set<string>();
  private targets: ResolvedTargetBase[];
  private timers: Array<ReturnType<typeof setInterval>> = [];

  constructor(
    store: ProbeStore,
    targets: ResolvedTargetBase[],
    maxConcurrentChecks?: number,
    retentionMs?: number,
    logger?: Logger,
  ) {
    this.store = store;
    this.targets = targets;
    this.semaphore = new Semaphore(maxConcurrentChecks ?? 20);
    this.retentionMs = retentionMs ?? 0;
    this.logger = logger ?? createNoopLogger();
    this.refreshCache();
    this.initStateCache();
  }

  start(): void {
    const groups = groupBy(this.targets, (t) => t.intervalMs);

    for (const [intervalMs, groupTargets] of Object.entries(groups)) {
      void this.probeGroup(groupTargets);

      const timer = setInterval(() => {
        void this.probeGroup(groupTargets);
      }, Number(intervalMs));

      this.timers.push(timer);
    }

    if (this.retentionMs > 0) {
      this.store.prune(this.retentionMs);
      const pruneTimer = setInterval(() => {
        this.store.prune(this.retentionMs);
      }, PRUNE_INTERVAL_MS);
      this.timers.push(pruneTimer);
    }
  }

  stop(): void {
    for (const timer of this.timers) {
      clearInterval(timer);
    }
    this.timers = [];
  }

  private initStateCache(): void {
    const latestMap = this.store.getLatestChecksMap();
    for (const [id, row] of latestMap) {
      this.lastMatched.set(id, row.matched === 1);
    }
  }

  private logCheckDebug(result: CheckResult): void {
    this.logger.debug({
      durationMs: result.durationMs,
      failureMessage: result.failure?.message ?? null,
      failurePhase: result.failure?.phase ?? null,
      matched: result.matched,
      targetId: result.targetId,
    });
  }

  private logStateChange(result: CheckResult): void {
    const previous = this.lastMatched.get(result.targetId);
    const current = result.matched;

    if (previous === undefined) {
      if (!current) {
        this.logger.warn(
          { durationMs: result.durationMs, failurePhase: result.failure?.phase, targetId: result.targetId },
          `目标首次 DOWN: ${result.targetId}`,
        );
      }
    } else if (previous && !current) {
      this.logger.warn(
        { durationMs: result.durationMs, failurePhase: result.failure?.phase, targetId: result.targetId },
        `目标状态变化 UP → DOWN: ${result.targetId}`,
      );
    } else if (!previous && current) {
      this.logger.info(
        { durationMs: result.durationMs, targetId: result.targetId },
        `目标恢复 DOWN → UP: ${result.targetId}`,
      );
    }

    this.lastMatched.set(result.targetId, current);
  }

  private async probeGroup(targets: ResolvedTargetBase[]): Promise<void> {
    const results = await Promise.allSettled(
      targets.map(async (target) => {
        await this.semaphore.acquire();
        try {
          return await this.runCheck(target);
        } finally {
          this.semaphore.release();
        }
      }),
    );

    for (const [index, result] of results.entries()) {
      if (result.status === "fulfilled") {
        this.writeResult(result.value);
        this.logStateChange(result.value);
        this.logCheckDebug(result.value);
      } else {
        const target = targets[index];
        if (target) {
          this.logger.error(
            { reason: formatReason(result.reason), targetId: target.id, targetType: target.type },
            `探针执行失败: ${formatReason(result.reason)}`,
          );
          this.writeResult({
            detail: null,
            durationMs: null,
            failure: errorFailure("internal", "engine", formatReason(result.reason)),
            matched: false,
            observation: null,
            targetId: target.id,
            timestamp: new Date().toISOString(),
          });
        }
      }
    }
  }

  private refreshCache(): void {
    this.targetIds.clear();
    for (const target of this.store.getTargets()) {
      this.targetIds.add(target.id);
    }
  }

  private async runCheck(target: ResolvedTargetBase): Promise<CheckResult> {
    const checker = checkerRegistry.get(target.type);
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), target.timeoutMs);

    try {
      return await checker.execute(target, { signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }

  private writeResult(result: CheckResult): void {
    if (!this.targetIds.has(result.targetId)) return;

    this.store.insertCheckResult({
      durationMs: result.durationMs,
      failure: result.failure,
      matched: result.matched,
      observation: result.observation ?? null,
      targetId: result.targetId,
      timestamp: result.timestamp,
    });
  }
}

function formatReason(reason: unknown): string {
  return isError(reason) ? reason.message : String(reason);
}
