import { createHash } from "node:crypto";
import type { CheckResult, ClientSite } from "../src/types.ts";
import { Store } from "./store.ts";
import { Mailer } from "./mail.ts";
import { inspectSite } from "./inspect.ts";
export class Monitor {
  queue = new Set<string>();
  active = new Set<string>();
  lastTickAt = "";
  stopped = false;
  private timer?: ReturnType<typeof setInterval>;
  private lastPrune = 0;
  constructor(
    public store: Store,
    public mailer: Mailer,
    public concurrency = 3,
    public inspector: (site: ClientSite) => Promise<CheckResult> = inspectSite,
    public retentionDays = 90,
  ) {}
  enqueue(id: string): boolean {
    if (
      this.stopped ||
      this.active.has(id) ||
      this.queue.has(id) ||
      !this.store.site(id)
    )
      return false;
    this.queue.add(id);
    this.drain();
    return true;
  }
  drain() {
    while (
      !this.stopped &&
      this.active.size < this.concurrency &&
      this.queue.size
    ) {
      const id = this.queue.values().next().value!;
      this.queue.delete(id);
      this.active.add(id);
      void this.run(id)
        .catch((e) =>
          console.error(
            "Monitor job failed:",
            e instanceof Error ? e.name : "error",
          ),
        )
        .finally(() => {
          this.active.delete(id);
          this.drain();
        });
    }
  }
  async run(id: string) {
    const site = this.store.site(id);
    if (!site) return;
    let result: CheckResult;
    try {
      result = await this.inspector(site);
    } catch {
      result = {
        url: site.url,
        hostname: new URL(site.url).hostname,
        status: "unknown",
        statusCode: 0,
        statusText: "Semakan gagal",
        responseTimeMs: 0,
        isWordPress: false,
        wpErrorType: "monitor_error",
        wpErrorMessage:
          "Pemantau gagal menjalankan semakan. Status website belum diketahui.",
        restApiStatus: "unknown",
        ssl: { valid: false, checked: false },
        checkedAt: new Date().toISOString(),
      };
    }
    const current = this.store.site(id);
    if (
      !current ||
      current.url !== site.url ||
      current.restApiUrl !== site.restApiUrl
    )
      return;
    const event = this.store.record(site, result);
    const settings = this.store.settings();
    const quiet =
      !!current.maintenanceUntil &&
      Date.parse(current.maintenanceUntil) > Date.now();
    if (!settings.autoEmailAlerts || quiet) return;
    const mail = (key: string, type: string, message: string) =>
      this.store.queueMail(key, {
        siteName: current.name,
        siteUrl: current.url,
        errorType: type,
        errorMessage: message,
        recipient: settings.adminEmail,
      });
    if (event.opened)
      mail(
        `incident:${event.opened.id}:down`,
        "Gangguan disahkan",
        event.opened.errorMessage,
      );
    if (event.recovered && settings.recoveryAlerts)
      mail(
        `incident:${event.recovered.id}:recovered`,
        "Website pulih",
        `Website boleh dicapai semula. Gangguan bermula ${event.recovered.openedAt}. ${result.wpErrorMessage || ""}`,
      );
    if (result.status === "warning" && settings.alertOnWarning) {
      const signature = createHash("sha256")
        .update(
          (result.warnings || [result.wpErrorMessage || ""])
            .map((w) => w.replace(/\d+/g, "#"))
            .join("|"),
        )
        .digest("hex")
        .slice(0, 16);
      mail(
        `warning:${id}:${new Date().toISOString().slice(0, 10)}:${signature}`,
        "Amaran website",
        result.wpErrorMessage || "Semak butiran website.",
      );
    }
  }
  tick() {
    if (this.stopped) return;
    this.lastTickAt = new Date().toISOString();
    const interval = this.store.settings().autoCheckIntervalMinutes * 60000;
    if (interval > 0)
      for (const site of this.store.sites()) {
        if (
          !site.lastResult ||
          Date.now() - Date.parse(site.lastResult.checkedAt) >= interval ||
          ((site.consecutiveFailures || 0) > 0 &&
            (site.consecutiveFailures || 0) <
              this.store.settings().failureThreshold &&
            Date.now() - Date.parse(site.lastResult.checkedAt) >= 30000)
        )
          this.enqueue(site.id);
      }
    if (Date.now() - this.lastPrune > 3600000) {
      this.store.prune(this.retentionDays);
      this.lastPrune = Date.now();
    }
    void this.mailer
      .flush()
      .catch(() => console.error("Mail queue unavailable."));
  }
  start() {
    this.tick();
    this.timer = setInterval(() => this.tick(), 10000);
  }
  stop() {
    this.stopped = true;
    if (this.timer) clearInterval(this.timer);
    this.queue.clear();
  }
}
