import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { createSkipCollector } from "../skipped";
import { type ActivityEvent, type ActivitySourceResult, type HookFiresSourceOptions } from "../types";

export const HOOK_FIRES_SOURCE = {
  id: "hook-fires",
  label: "hook fires",
  category: "guard",
} as const;

export const HOOK_FIRES_SOURCE_ID = HOOK_FIRES_SOURCE.id;
const HOOK_FIRES_SOURCE_LABEL = HOOK_FIRES_SOURCE.label;

interface HookFireRaw {
  ts?: unknown;
  event?: unknown;
  module?: unknown;
  action?: unknown;
}

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

function asString(value: unknown): string | undefined {
  if (typeof value !== "string") return undefined;
  return value.trim().length === 0 ? undefined : value;
}

function coverage(path: string, status: "absent" | "error", error?: string): ActivitySourceResult {
  return {
    events: [],
    skippedEntries: [],
    skippedTruncated: false,
    coverage: {
      id: HOOK_FIRES_SOURCE_ID,
      label: HOOK_FIRES_SOURCE_LABEL,
      category: HOOK_FIRES_SOURCE.category,
      path,
      status,
      records: 0,
      totalRecords: 0,
      skipped: 0,
      ...(error === undefined ? {} : { error }),
    },
  };
}

export function readHookFiresSource(options: HookFiresSourceOptions = {}): ActivitySourceResult {
  const {
    path = defaultHookFiresPath(),
    readFileImpl = (path) => readFileSync(path, "utf8"),
    existsSyncImpl = existsSync,
    fromMs,
    toMs,
    collectSkipped = false,
  } = options;

  if (!existsSyncImpl(path)) return coverage(path, "absent");

  let raw: string;
  try {
    raw = readFileImpl(path);
  } catch (error) {
    return coverage(path, "error", error instanceof Error ? error.message : String(error));
  }

  let records = 0;
  let totalRecords = 0;
  const skips = createSkipCollector(HOOK_FIRES_SOURCE_ID, collectSkipped);
  let earliestMs: number | undefined;
  let latestMs: number | undefined;
  let ordinal = 0;
  let lineNumber = 0;
  const events: ActivityEvent[] = [];

  for (const line of raw.split("\n")) {
    lineNumber += 1;
    const trimmed = line.trim();
    if (!trimmed) continue;
    ordinal += 1;
    const where = { path, line: lineNumber };

    let parsed: unknown;
    try {
      parsed = JSON.parse(trimmed);
    } catch {
      skips.record("unparseable-json", where, trimmed);
      continue;
    }
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
      skips.record("not-an-object", where, trimmed);
      continue;
    }

    const record = parsed as HookFireRaw;
    const event = asString(record.event);
    const module = asString(record.module);
    const action = asString(record.action);
    const tsRaw = asString(record.ts);
    const tsMs = tsRaw === undefined ? Number.NaN : Date.parse(tsRaw);
    if (!event || !module || !action || tsRaw === undefined) {
      skips.record(
        "missing-fields",
        where,
        trimmed,
        `${!event ? "event" : !module ? "module" : !action ? "action" : "ts"} is absent or blank`,
      );
      continue;
    }
    if (!Number.isFinite(tsMs)) {
      skips.record("invalid-timestamp", where, trimmed, `ts=${tsRaw} does not parse as ISO 8601`);
      continue;
    }

    totalRecords += 1;
    if (earliestMs === undefined || tsMs < earliestMs) earliestMs = tsMs;
    if (latestMs === undefined || tsMs > latestMs) latestMs = tsMs;
    if (fromMs !== undefined && tsMs < fromMs) continue;
    if (toMs !== undefined && tsMs > toMs) continue;

    const hookEvent: ActivityEvent = {
      id: `${HOOK_FIRES_SOURCE_ID}:${ordinal}`,
      ts: new Date(tsMs).toISOString(),
      category: HOOK_FIRES_SOURCE.category,
      source: HOOK_FIRES_SOURCE_ID,
      actor: "system",
      severity: action === "deny" ? "notice" : "info",
      title: `${action} (${module}) ${event}`,
      dedupeKey: `${module}:${event}:${action}:${tsRaw}`,
      detail: { event, module, action },
    };

    events.push(hookEvent);
    records += 1;
  }

  return {
    events,
    skippedEntries: skips.entries,
    skippedTruncated: skips.truncated,
    coverage: {
      id: HOOK_FIRES_SOURCE_ID,
      label: HOOK_FIRES_SOURCE_LABEL,
      category: HOOK_FIRES_SOURCE.category,
      path,
      status: "ok",
      records,
      totalRecords,
      skipped: skips.count,
      ...(earliestMs === undefined
        ? {}
        : { earliest: new Date(earliestMs).toISOString(), latest: new Date(latestMs ?? earliestMs).toISOString() }),
    },
  };
}
