import { existsSync, readdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { Adapter } from "../adapter";
import type { ActionRef, AdapterResult, Item, Kind, Panel, Severity } from "../schema";
import type { RequestsStore } from "../requests/requests-store";

export type AgentRuntime = "claude" | "codex" | "cursor-agent";

/** Ledger entry written by the sibling session-tracking component (schemaVersion 1). */
export interface AgentSessionEntry {
  schemaVersion: 1;
  ledgerId: string;
  runtime: AgentRuntime;
  startedAt: string;
  host: string;
  bootId: string | null;
  cwd: string;
  repoRoot: string | null;
  project: string | null;
  branch: string | null;
  worktree: boolean;
  tty: string | null;
  launcherPid: number;
  parentLedgerId: string | null;
  launchedBy?: string | null;
  /** Provider account slug the runtime's staged credentials came from; null on the default seat. */
  account?: string | null;
  mux: { kind: "dtach" | "tmux" | null; socket: string | null; target: string | null };
  pid: number | null;
  pidStartTicks: number | null;
  sessionId: string | null;
  transcriptPath: string | null;
  lastHeartbeatAt: string | null;
  finishedAt: string | null;
  finishReason: string | null;
  rescuedAt?: string | null;
  rescueRef?: string | null;
  rescueCommit?: string | null;
  rescuedPaths?: number | null;
}

export type AgentSessionState =
  | "ALIVE-WORKING"
  | "ALIVE-IDLE"
  | "DETACHED-ALIVE"
  | "ORPHANED"
  | "FINISHED";

export interface ClassifiedAgentSession extends AgentSessionEntry {
  state: AgentSessionState;
  attached: boolean;
  lastProgressAt: string | null;
  idleMs: number | null;
  dirtyCount: number | null;
  evidence: string;
}

export interface AgentSessionsProjectGroup {
  project: string | null;
  sessions: ClassifiedAgentSession[];
}

export interface AgentSessionsPanelData {
  generatedAt: string;
  sessions: AgentSessionsProjectGroup[];
}

export type ClassifyImpl = (
  entries: AgentSessionEntry[],
  opts: { now: () => number },
) => Promise<ClassifiedAgentSession[]>;

export interface CreateAgentSessionsAdapterOptions {
  id?: string;
  interval?: number;
  /** Base ledger directory; entries live under `${dir}/sessions/*.json`. */
  dir: string;
  now?: () => number;
  /** Injectable for tests; defaults to fs.readFileSync. */
  readFileImpl?: (path: string) => string;
  /** Injectable for tests; defaults to fs.readdirSync. */
  readdirImpl?: (path: string) => string[];
  /** Injectable for tests; defaults to fs.existsSync. */
  existsImpl?: (path: string) => boolean;
  /** Injectable for tests; defaults to a lazy dynamic import of the canonical classifier. */
  classifyImpl?: ClassifyImpl;
  /** Optional request registry. Missing registry never blocks session telemetry. */
  requests?: Pick<RequestsStore, "orphanSession">;
  /** How long after finishedAt a FINISHED session still appears in the panel. */
  finishedRetentionMs?: number;
}

const DEFAULT_INTERVAL_MS = 30_000;
const DEFAULT_FINISHED_RETENTION_MS = 24 * 60 * 60 * 1000;
const CLASSIFIER_MODULE_PATH = join(homedir(), ".claude", "lib", "agent-session-reader.mjs");

async function defaultClassifyImpl(
  entries: AgentSessionEntry[],
  opts: { now: () => number },
): Promise<ClassifiedAgentSession[]> {
  const mod = (await import(CLASSIFIER_MODULE_PATH)) as { classify: ClassifyImpl };
  return mod.classify(entries, opts);
}

function isSchemaV1(value: unknown): value is AgentSessionEntry {
  return (
    Boolean(value) &&
    typeof value === "object" &&
    (value as { schemaVersion?: unknown }).schemaVersion === 1 &&
    typeof (value as { ledgerId?: unknown }).ledgerId === "string"
  );
}

function formatDuration(ms: number): string {
  const totalMinutes = Math.max(0, Math.round(ms / 60_000));
  const hours = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;
  return hours === 0 ? `${minutes}m` : `${hours}h${minutes}m`;
}

function severityFor(c: ClassifiedAgentSession): Severity {
  if (c.state === "ORPHANED") return (c.dirtyCount ?? 0) > 0 ? "act" : "warn";
  if (c.state === "DETACHED-ALIVE") return "warn";
  return "info";
}

function kindFor(c: ClassifiedAgentSession): Kind {
  return c.state === "ORPHANED" ? "alert" : "progress";
}

function titleFor(c: ClassifiedAgentSession): string {
  const where = c.project ?? c.cwd;
  switch (c.state) {
    case "ORPHANED": {
      const dirty = c.dirtyCount ?? 0;
      const suffix = dirty > 0 ? ` (${dirty} uncommitted file${dirty === 1 ? "" : "s"})` : "";
      return `Abandoned ${c.runtime} session in ${where}${suffix}`;
    }
    case "DETACHED-ALIVE":
      return `Detached ${c.runtime} session in ${where}`;
    case "ALIVE-IDLE":
      return `Idle ${c.runtime} session in ${where}`;
    default:
      return `Active ${c.runtime} session in ${where}`;
  }
}

function detailFor(c: ClassifiedAgentSession): string {
  const parts: string[] = [];
  if (c.branch) parts.push(`branch ${c.branch}`);
  parts.push(`cwd ${c.cwd}`);
  if (c.idleMs !== null && c.idleMs !== undefined) parts.push(`idle ${formatDuration(c.idleMs)}`);
  if (c.transcriptPath) parts.push(`transcript ${c.transcriptPath}`);
  return parts.join(" — ");
}

function actionsFor(c: ClassifiedAgentSession): ActionRef[] {
  if (c.state === "ORPHANED") {
    return [{ verb: "resume", args: { ledgerId: c.ledgerId }, label: "Resume session" }];
  }
  return [{ verb: "attach", args: { ledgerId: c.ledgerId }, label: "Reopen session", recommended: true }];
}

function sessionTs(c: ClassifiedAgentSession): number {
  const iso = c.lastProgressAt ?? c.startedAt;
  const parsed = Date.parse(iso);
  return Number.isFinite(parsed) ? parsed : 0;
}

function groupByProject(sessions: ClassifiedAgentSession[]): AgentSessionsProjectGroup[] {
  const ordered = [...sessions].sort((a, b) => sessionTs(b) - sessionTs(a));
  const byProject = new Map<string | null, ClassifiedAgentSession[]>();
  for (const session of ordered) {
    const key = session.project ?? null;
    const list = byProject.get(key) ?? [];
    list.push(session);
    byProject.set(key, list);
  }
  return [...byProject.entries()]
    .map(([project, grouped]) => ({ project, sessions: grouped }))
    .sort((a, b) => sessionTs(b.sessions[0]!) - sessionTs(a.sessions[0]!));
}

export function createAgentSessionsAdapter(opts: CreateAgentSessionsAdapterOptions): Adapter {
  const id = opts.id ?? "agent-sessions";
  const interval = opts.interval ?? DEFAULT_INTERVAL_MS;
  const now = opts.now ?? Date.now;
  const readFileImpl = opts.readFileImpl ?? ((path: string) => readFileSync(path, "utf8"));
  const readdirImpl = opts.readdirImpl ?? ((path: string) => readdirSync(path));
  const existsImpl = opts.existsImpl ?? ((path: string) => existsSync(path));
  const classifyImpl = opts.classifyImpl ?? defaultClassifyImpl;
  const finishedRetentionMs = opts.finishedRetentionMs ?? DEFAULT_FINISHED_RETENTION_MS;
  const sessionsDir = join(opts.dir, "sessions");
  const warnedPaths = new Set<string>();

  function warnCorruptOnce(path: string, error: unknown): void {
    if (warnedPaths.has(path)) return;
    warnedPaths.add(path);
    const message = error instanceof Error ? error.message : String(error);
    console.warn(`[agent-sessions] ignored corrupt ${path}: ${message}`);
  }

  function readEntries(): AgentSessionEntry[] {
    if (!existsImpl(sessionsDir)) return [];
    const entries: AgentSessionEntry[] = [];
    for (const name of readdirImpl(sessionsDir)) {
      if (!name.endsWith(".json")) continue;
      const path = join(sessionsDir, name);
      let parsed: unknown;
      try {
        parsed = JSON.parse(readFileImpl(path));
      } catch (error) {
        warnCorruptOnce(path, error);
        continue;
      }
      if (!isSchemaV1(parsed)) {
        warnCorruptOnce(path, new Error("missing/invalid schemaVersion"));
        continue;
      }
      entries.push(parsed);
    }
    return entries;
  }

  return {
    id,
    interval,
    async poll(): Promise<AdapterResult> {
      const nowMs = now();
      const ts = new Date(nowMs).toISOString();
      const entries = readEntries();
      const classified = await classifyImpl(entries, { now });

      // sweep() records the authoritative recovery signal only after the kernel proves
      // an observed runtime exited and its dirty checkout was snapshotted. A classifier
      // ORPHANED row is only a launch birth-window with no process observed yet.
      if (opts.requests) {
        for (const session of classified) {
          const rescuedPaths = session.rescuedPaths ?? 0;
          if (
            session.state !== "FINISHED"
            || session.finishReason !== "process exited"
            || rescuedPaths <= 0
            || !session.sessionId
          ) continue;
          try {
            opts.requests.orphanSession(session.sessionId, rescuedPaths);
          } catch (error) {
            // Session telemetry remains available when request reconciliation is
            // absent, racing, or temporarily unhealthy.
            console.warn(`[agent-sessions] request orphan reconciliation failed for ${session.ledgerId}: ${error instanceof Error ? error.message : String(error)}`);
          }
        }
      }

      const items: Item[] = classified
        .filter((c) => c.state !== "FINISHED")
        .map((c) => ({
          id: `agent-session:${c.ledgerId}`,
          source: id,
          project: c.project ?? undefined,
          reconciliationScope: "agent-sessions",
          severity: severityFor(c),
          kind: kindFor(c),
          title: titleFor(c),
          detail: detailFor(c),
          ts: c.lastProgressAt ?? c.startedAt,
          actions: actionsFor(c),
        }));

      const panelSessions = classified.filter((c) => {
        if (c.state !== "FINISHED") return true;
        if (!c.finishedAt) return false;
        const finishedMs = Date.parse(c.finishedAt);
        return Number.isFinite(finishedMs) && nowMs - finishedMs <= finishedRetentionMs;
      });

      const panel: Panel = {
        id: "agent-sessions",
        ts,
        data: {
          generatedAt: ts,
          sessions: groupByProject(panelSessions),
        } satisfies AgentSessionsPanelData,
      };

      return { items, panels: [panel], completeScopes: ["agent-sessions"] };
    },
  };
}
