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

export const CONTROLLER_SOURCE = {
  id: "controller",
  label: "controller events",
  category: "buildbox",
} as const;

export const CONTROLLER_SOURCE_ID = CONTROLLER_SOURCE.id;
const CONTROLLER_SOURCE_LABEL = "controller events";

interface ControllerRaw {
  ts?: unknown;
  job?: unknown;
  repo?: unknown;
  host?: unknown;
  stage?: unknown;
  reason?: unknown;
  rc?: unknown;
  durationSeconds?: unknown;
}

export function defaultControllerPath(): string {
  return join(homedir(), ".config", "overdeck", "controller", "events.jsonl");
}

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

function asNumber(value: unknown): number | undefined {
  if (typeof value === "number" && Number.isFinite(value)) {
    return value;
  }
  return undefined;
}

function severityFromController(record: ControllerRaw, stage: string | undefined): "debug" | "info" | "notice" | "warn" | "error" {
  const rc = asNumber(record.rc);
  if (rc !== undefined && rc !== 0) {
    return "error";
  }
  const haystack = `${stage ?? ""} ${asString(record.reason) ?? ""}`.toLowerCase();
  return haystack.includes("fail") || haystack.includes("invalid") || haystack.includes("error")
    ? "error"
    : "info";
}

function title(record: ControllerRaw, stage: string | undefined): string {
  const head = [];
  const host = asString(record.host);
  const repo = asString(record.repo);
  if (host) {
    head.push(host);
  }
  if (repo) {
    head.push(repo);
  }
  const body = `${stage ?? "controller event"}${asString(record.reason) ? ` — ${asString(record.reason)}` : ""}`;
  return head.length === 0 ? body : `${head.join(" · ")} · ${body}`;
}

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

  if (!existsSyncImpl(path)) {
    return {
      skippedEntries: [],
      skippedTruncated: false,
      coverage: {
        id: CONTROLLER_SOURCE_ID,
        label: CONTROLLER_SOURCE_LABEL,
        category: CONTROLLER_SOURCE.category,
        path,
        status: "absent",
        records: 0,
        totalRecords: 0,
        skipped: 0,
      },
      events: [],
    };
  }

  let raw: string;
  try {
    raw = readFileImpl(path);
  } catch (error) {
    return {
      skippedEntries: [],
      skippedTruncated: false,
      coverage: {
        id: CONTROLLER_SOURCE_ID,
        label: CONTROLLER_SOURCE_LABEL,
        category: CONTROLLER_SOURCE.category,
        path,
        status: "error",
        records: 0,
        totalRecords: 0,
        skipped: 0,
        error: error instanceof Error ? error.message : String(error),
      },
      events: [],
    };
  }

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

  for (const line of raw.split("\n")) {
    lineNumber += 1;
    const trimmed = line.trim();
    if (!trimmed) continue;
    ordinal += 1;
    const where = { path: 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 ControllerRaw;
    const ts = asString(record.ts);
    if (!ts) {
      skips.record("missing-fields", where, trimmed, "ts is absent or blank");
      continue;
    }
    const tsMs = Date.parse(ts);
    if (!Number.isFinite(tsMs)) {
      skips.record("invalid-timestamp", where, trimmed, `ts=${ts}`);
      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 stage = asString(record.stage);
    const event: ActivityEvent = {
      id: `${CONTROLLER_SOURCE_ID}:${ordinal}`,
      ts,
        category: CONTROLLER_SOURCE.category,
      source: CONTROLLER_SOURCE_ID,
      actor: "system",
      severity: severityFromController(record, stage),
      title: title(record, stage),
    };

    const project = asString(record.repo);
    if (project !== undefined) {
      event.project = project;
    }
    const eventHost = asString(record.host);
    if (eventHost !== undefined) {
      event.host = eventHost;
    }
    const session = asString(record.job);
    if (session !== undefined) {
      event.session = session;
    }
    const durationSeconds = asNumber(record.durationSeconds);
    if (durationSeconds !== undefined && durationSeconds > 0) {
      event.durationMs = Math.round(durationSeconds * 1000);
    }
    const rc = asNumber(record.rc);
    if (rc !== undefined) {
      event.rc = rc;
    }
    const reason = asString(record.reason);
    if (eventHost !== undefined || stage !== undefined || reason !== undefined) {
      event.dedupeKey = `${eventHost ?? ""}:${stage ?? ""}:${reason ?? ""}`;
    }

    const redacted = redactBrowserValue(parsed);
    if (redacted && typeof redacted === "object" && !Array.isArray(redacted)) {
      event.detail = redacted as Record<string, unknown>;
    }

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

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