import { homedir, hostname } from "node:os";
import type { Adapter } from "../adapter";
import type { AdapterResult } from "../schema";
import {
  resolveAttach,
  resolveScreenCapability,
  resumeKey,
  type AttachDescriptor,
} from "../sessions/attach";
import { hostFactsFor, hostsRegistryPath, readHostRegistry, type HostFacts } from "../sessions/hosts";
import {
  defaultLedgerDir,
  isAttachableState,
  readLedger,
  type LedgerReadResult,
  type SessionRecord,
} from "../sessions/ledger";
import { tmuxBinary } from "../sessions/tmux";
import { probeRemoteHosts, type BoxResidentSession, type RemoteHostProbe } from "../sessions/remote";

export interface HostProbeStatus {
  host: string;
  status: "pending" | "ok" | "failed" | "not-reachable";
  error: string | null;
  at: string;
  sessionCount: number;
}

export const SESSIONS_PANEL_ID = "sessions";

export interface SessionsAdapterOptions {
  id?: string;
  interval?: number;
  ledgerDir?: string;
  /** False when the collector is not bound to loopback — browser attach stays off. */
  attachAllowed: boolean;
  attachBlockedReason?: string;
  readLedgerImpl?: (dir: string) => LedgerReadResult | Promise<LedgerReadResult>;
  tmuxBinaryImpl?: () => string | null;
  readHostRegistryImpl?: () => { hosts: HostFacts[]; missing: boolean };
  hostnameImpl?: () => string;
  homedirImpl?: () => string;
  now?: () => number;
  probeInterval?: number;
  probeRemoteImpl?: (options: { localHost: string }) => Promise<RemoteHostProbe[]>;
}

export interface SessionView extends SessionRecord {
  /** Absolute project root — the grouping key. Falls back to cwd when no repo is known. */
  project: string;
  /** Registry facts for `host`; null when the recorder stamped no host. */
  hostFacts: HostFacts | null;
  /** How to reconnect to this session, or why it is impossible. */
  attach: AttachDescriptor;
  /** Whether the collector can render this session's live screen in the browser. */
  attachable: boolean;
  /** Why the browser screen is unavailable; absent when `attachable` is true. */
  attachBlockedReason?: string;
}

export interface SessionsPanelData {
  ledgerDir: string;
  ledgerMissing: boolean;
  unreadableFiles: string[];
  tmuxAvailable: boolean;
  attachEnabled: boolean;
  attachBlockedReason?: string;
  /** Host the collector runs on; every session it can see is enrolled from here. */
  localHost: string;
  /** Home directory on the collector host, so the UI can shorten project paths. */
  homeDir: string | null;
  /** Static host registry, so the UI can name hosts without probing any of them. */
  hosts: HostFacts[];
  hostsRegistryPath: string;
  hostsRegistryMissing: boolean;
  hostProbes: HostProbeStatus[];
  sessions: SessionView[];
  /** Sessions living in a persistent container on a box (docs/plans/2026-08-15-laptop-as-terminal.md
   * S2/S4) — no ledger entry exists for these, so they never appear in `sessions`. */
  boxResident: BoxResidentSession[];
}

const DEFAULT_INTERVAL = 10_000;

/**
 * Grouping key. The ledger's repo root is a `git rev-parse` walk upward, so a session
 * started outside any checkout resolves to whatever repo happens to contain the home
 * directory — a group literally named after the user's home. That is honest but reads as
 * a bug, so a repo root at or above home is refused and the session's own cwd is used.
 */
function projectRoot(record: SessionRecord, home: string | null): string {
  const root = record.repoRoot;
  if (!root) return record.cwd;
  if (home && (root === home || `${home}/`.startsWith(`${root}/`))) return record.cwd;
  return root;
}

function sortKeyMs(record: SessionRecord): number {
  const raw = record.lastActiveAt ?? record.lastProgressAt ?? record.lastHeartbeatAt ??
    record.startedAt;
  if (!raw) return 0;
  const parsed = Date.parse(raw);
  return Number.isNaN(parsed) ? 0 : parsed;
}

/**
 * Serves the agent-session ledger. Panel data only: the ledger is the source of
 * truth, and this adapter never synthesizes a session, a state, a timestamp, a title or
 * a launcher — an empty ledger yields an empty list with `ledgerMissing` telling the UI why.
 */
const MAX_FINISHED_SESSIONS = 100;

export function createSessionsAdapter(options: SessionsAdapterOptions): Adapter {
  const id = options.id ?? "sessions";
  const interval = options.interval ?? DEFAULT_INTERVAL;
  const ledgerDir = options.ledgerDir ?? defaultLedgerDir();
  const readLedgerImpl = options.readLedgerImpl ?? readLedger;
  const tmuxBinaryImpl = options.tmuxBinaryImpl ?? tmuxBinary;
  const readHostRegistryImpl = options.readHostRegistryImpl ?? (() => readHostRegistry());
  const hostnameImpl = options.hostnameImpl ?? hostname;
  const homedirImpl = options.homedirImpl ?? homedir;
  const now = options.now ?? Date.now;
  const probeInterval = options.probeInterval ?? 60_000;
  const probeRemoteImpl = options.probeRemoteImpl ?? probeRemoteHosts;
  let cachedProbes: RemoteHostProbe[] = [];
  let probeInFlight: Promise<void> | null = null;
  let lastProbeStartedAt = Number.NEGATIVE_INFINITY;

  async function poll(): Promise<AdapterResult> {
    const ledger = await readLedgerImpl(ledgerDir);
    const tmuxAvailable = tmuxBinaryImpl() !== null;
    const registry = readHostRegistryImpl();
    const localHost = hostnameImpl();
    const home = homedirImpl() || null;
    const snapshot = {
      localHost,
      hosts: registry.hosts,
      path: hostsRegistryPath(),
      missing: registry.missing,
    };

    const currentTime = now();
    if (!probeInFlight && currentTime - lastProbeStartedAt >= probeInterval) {
      lastProbeStartedAt = currentTime;
      probeInFlight = probeRemoteImpl({ localHost })
        .then((result) => { cachedProbes = result; })
        .catch((error) => {
          cachedProbes = [{ host: "registry", status: "failed", error: String(error).split(/\r?\n/, 1)[0]!, at: new Date(currentTime).toISOString(), sessions: [], resident: [] }];
        })
        .finally(() => { probeInFlight = null; });
    }

    const remoteById = new Map(cachedProbes.flatMap((probe) => probe.sessions).map((record) => [record.id, record]));
    const mergedRecords = new Map(ledger.sessions.map((record) => [record.id, record]));
    for (const [recordId, record] of remoteById) mergedRecords.set(recordId, record);

    const liveResumeIds = new Map<string, string>();
    for (const record of mergedRecords.values()) {
      if (!isAttachableState(record.state)) continue;
      const key = resumeKey(record);
      if (key && !liveResumeIds.has(key)) liveResumeIds.set(key, record.id);
    }

    const records = [...mergedRecords.values()];
    const finished = records
      .filter((record) => record.state === "FINISHED")
      .sort((a, b) => sortKeyMs(b) - sortKeyMs(a))
      .slice(0, MAX_FINISHED_SESSIONS);
    const sessions: SessionView[] = records
      .filter((record) => record.state !== "FINISHED")
      .concat(finished)
      .map((record) => {
        const hostFacts = hostFactsFor(snapshot, record.host);
        const context = { localHost, hostFacts, liveResumeIds };
        const screen = resolveScreenCapability(record, {
          ...context,
          tmuxAvailable,
          attachAllowed: options.attachAllowed,
          ...(options.attachBlockedReason ? { attachBlockedReason: options.attachBlockedReason } : {}),
        });
        return {
          ...record,
          project: projectRoot(record, home),
          hostFacts,
          attach: resolveAttach(record, context),
          attachable: screen.attachable,
          ...(screen.reason ? { attachBlockedReason: screen.reason } : {}),
        };
      })
      .sort((a, b) => sortKeyMs(b) - sortKeyMs(a));

    const data: SessionsPanelData = {
      ledgerDir: ledger.dir,
      ledgerMissing: ledger.missing,
      unreadableFiles: ledger.unreadable,
      tmuxAvailable,
      attachEnabled: options.attachAllowed,
      ...(options.attachAllowed ? {} : { attachBlockedReason: options.attachBlockedReason ?? "browser attach disabled" }),
      localHost,
      homeDir: home,
      hosts: registry.hosts,
      hostsRegistryPath: snapshot.path,
      hostsRegistryMissing: registry.missing,
      hostProbes: registry.hosts
        .filter((host) => host.name !== localHost)
        .map((host) => {
          const probe = cachedProbes.find((entry) => entry.host === host.name);
          if (probe) return { host: probe.host, status: probe.status, error: probe.error, at: probe.at, sessionCount: probe.sessions.length };
          return {
            host: host.name,
            status: host.state === "reachable" ? "pending" as const : "not-reachable" as const,
            error: host.state === "reachable" ? null : `registry state: ${host.state}`,
            at: new Date(currentTime).toISOString(),
            sessionCount: 0,
          };
        }),
      sessions,
      boxResident: cachedProbes.flatMap((probe) => probe.resident),
    };

    return {
      items: [],
      panels: [{ id: SESSIONS_PANEL_ID, ts: new Date(now()).toISOString(), data }],
    };
  }

  return { id, interval, poll };
}

/** Looks the session up in the current panel snapshot — the only authority for a
 * tmux target. A client supplies a session id and never a socket path or pane. */
export function sessionFromPanel(panelData: unknown, sessionId: string): SessionView | null {
  const data = panelData as SessionsPanelData | undefined;
  if (!data || !Array.isArray(data.sessions)) return null;
  return data.sessions.find((session) => session.id === sessionId) ?? null;
}
