import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { z } from "zod";

export interface DispatcherRegistryEntry {
  id: string;
  matcher: string;
  timeoutMs?: number;
  mainOnly?: boolean;
}

export interface DispatcherRegistry {
  [event: string]: DispatcherRegistryEntry[] | undefined;
}

export type HookFireStatus = "OBSERVING" | "ACTIVE" | "SUSPECT" | "REMOVAL_CANDIDATE";

export interface HookFireModuleRow {
  module: string;
  event: string;
  matcher: string | null;
  inManifest: boolean;
  fires24h: number;
  fires7d: number;
  fires30d: number;
  lastFiredAt: string | null;
  status: HookFireStatus;
}

export interface HookFireStatsResponse {
  generatedAt: string;
  path: string;
  status: "ok" | "absent" | "error";
  error?: string;
  earliestObservedAt: string | null;
  daysObserved: number;
  modules: HookFireModuleRow[];
}

export interface HookFireStatsOptions {
  jsonlPath?: string;
  registryPath?: string;
  readFileImpl?: (path: string) => string;
  existsSyncImpl?: (path: string) => boolean;
  importRegistryImpl?: (path: string) => Promise<{ REGISTRY: DispatcherRegistry }>;
  now?: () => Date;
}

const HookFireRecordSchema = z.object({
  ts: z.string().min(1),
  event: z.string().min(1),
  module: z.string().min(1),
  action: z.string().min(1),
});

const DAY_MS = 24 * 60 * 60 * 1000;
const WINDOW_7D_MS = 7 * DAY_MS;
const WINDOW_30D_MS = 30 * DAY_MS;

export function defaultHookFiresJsonlPath(): string {
  const stateHome = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
  return join(stateHome, "overdeck", "hook-fires.jsonl");
}

export function defaultDispatcherRegistryPath(): string {
  return join(homedir(), "Projects", "overdeck", "modules", "workstation", "claude", "hooks", "lib", "dispatcher-registry.mjs");
}

interface ManifestModule {
  events: Set<string>;
  matcher: string | null;
}

function collectManifestModules(registry: DispatcherRegistry): Map<string, ManifestModule> {
  const modules = new Map<string, ManifestModule>();
  for (const [event, entries] of Object.entries(registry)) {
    if (!Array.isArray(entries)) continue;
    for (const entry of entries) {
      const id = entry.id;
      const existing = modules.get(id);
      if (existing) {
        existing.events.add(event);
      } else {
        modules.set(id, { events: new Set([event]), matcher: entry.matcher ?? null });
      }
    }
  }
  return modules;
}

interface Accumulator {
  fires24h: number;
  fires7d: number;
  fires30d: number;
  lastFiredMs: number | null;
  events: Set<string>;
}

function newAccumulator(): Accumulator {
  return { fires24h: 0, fires7d: 0, fires30d: 0, lastFiredMs: null, events: new Set() };
}

function statusFor(fires7d: number, fires30d: number, daysObserved: number): HookFireStatus {
  if (fires7d > 0) return "ACTIVE";
  if (daysObserved < 7) return "OBSERVING";
  if (daysObserved >= 30 && fires30d === 0) return "REMOVAL_CANDIDATE";
  return "SUSPECT";
}

export async function computeHookFireStats(options: HookFireStatsOptions = {}): Promise<HookFireStatsResponse> {
  const jsonlPath = options.jsonlPath ?? defaultHookFiresJsonlPath();
  const registryPath = options.registryPath ?? defaultDispatcherRegistryPath();
  const readFileImpl = options.readFileImpl ?? ((path: string) => readFileSync(path, "utf8"));
  const existsSyncImpl = options.existsSyncImpl ?? existsSync;
  const importRegistryImpl = options.importRegistryImpl
    ?? ((path: string) => import(pathToFileURL(path).href) as Promise<{ REGISTRY: DispatcherRegistry }>);
  const nowMs = (options.now ?? (() => new Date()))().getTime();
  const generatedAt = new Date(nowMs).toISOString();

  let manifestModules: Map<string, ManifestModule>;
  try {
    const loaded = await importRegistryImpl(registryPath);
    manifestModules = collectManifestModules(loaded.REGISTRY);
  } catch {
    manifestModules = new Map();
  }

  const accumulators = new Map<string, Accumulator>();
  let earliestMs: number | undefined;

  let readStatus: HookFireStatsResponse["status"] = "ok";
  let readError: string | undefined;

  if (!existsSyncImpl(jsonlPath)) {
    readStatus = "absent";
  } else {
    let raw: string;
    try {
      raw = readFileImpl(jsonlPath);
    } catch (error) {
      readStatus = "error";
      readError = error instanceof Error ? error.message : String(error);
      raw = "";
    }
    for (const line of raw.split("\n")) {
      const trimmed = line.trim();
      if (!trimmed) continue;
      let parsed: unknown;
      try {
        parsed = JSON.parse(trimmed);
      } catch {
        continue;
      }
      const result = HookFireRecordSchema.safeParse(parsed);
      if (!result.success) continue;
      const tsMs = Date.parse(result.data.ts);
      if (!Number.isFinite(tsMs)) continue;

      if (earliestMs === undefined || tsMs < earliestMs) earliestMs = tsMs;

      const moduleId = result.data.module;
      const acc = accumulators.get(moduleId) ?? newAccumulator();
      acc.events.add(result.data.event);
      if (acc.lastFiredMs === null || tsMs > acc.lastFiredMs) acc.lastFiredMs = tsMs;
      const ageMs = nowMs - tsMs;
      if (ageMs <= DAY_MS) acc.fires24h += 1;
      if (ageMs <= WINDOW_7D_MS) acc.fires7d += 1;
      if (ageMs <= WINDOW_30D_MS) acc.fires30d += 1;
      accumulators.set(moduleId, acc);
    }
  }

  const daysObserved = earliestMs === undefined ? 0 : Math.floor((nowMs - earliestMs) / DAY_MS);

  const moduleIds = new Set<string>([...manifestModules.keys(), ...accumulators.keys()]);
  const modules: HookFireModuleRow[] = [...moduleIds].map((moduleId) => {
    const manifestEntry = manifestModules.get(moduleId);
    const acc = accumulators.get(moduleId);
    const events = manifestEntry ? [...manifestEntry.events] : acc ? [...acc.events] : [];
    return {
      module: moduleId,
      event: events.length > 0 ? events.join(", ") : "unknown",
      matcher: manifestEntry?.matcher ?? null,
      inManifest: manifestEntry !== undefined,
      fires24h: acc?.fires24h ?? 0,
      fires7d: acc?.fires7d ?? 0,
      fires30d: acc?.fires30d ?? 0,
      lastFiredAt: acc?.lastFiredMs != null ? new Date(acc.lastFiredMs).toISOString() : null,
      status: statusFor(acc?.fires7d ?? 0, acc?.fires30d ?? 0, daysObserved),
    };
  });

  modules.sort((a, b) => a.module.localeCompare(b.module));

  return {
    generatedAt,
    path: jsonlPath,
    status: readStatus,
    ...(readError === undefined ? {} : { error: readError }),
    earliestObservedAt: earliestMs === undefined ? null : new Date(earliestMs).toISOString(),
    daysObserved,
    modules,
  };
}
