import { existsSync, readFileSync, statSync } from "node:fs";
import { dirname, join } from "node:path";
import type { Adapter } from "../adapter";
import type { AdapterResult, Item, Panel } from "../schema";

export type SystrayProvider = "codex" | "claude";

/** Shape written by systray-ai's HealthSnapshotStore._serialize_snapshots (health_store.py). */
interface SystraySpend {
  amount: number | null;
  limit: number | null;
  currency: string | null;
  display: string | null;
}

interface SystraySnapshot {
  status: string;
  primary_used_pct: number | null;
  secondary_used_pct: number | null;
  primary_reset_at: number | null;
  secondary_reset_at: number | null;
  checked_at: number | null;
  detail: string | null;
  spend: SystraySpend | null;
}

type SystrayFile = Record<string, SystraySnapshot>;

interface SystrayRegistryEntry {
  slug: string;
  alias?: string;
}

interface SystrayAccountsFile {
  accounts: SystrayRegistryEntry[];
}

export interface SystrayThresholds {
  /** Alert when account's used percent is at or above this value. */
  percentWarn: number;
  /** Alert when the linear-projection ETA to cap is under this many minutes. */
  etaWarnMinutes: number;
}

export interface AccountLimitView {
  /** Unique across providers — `${provider}:${accountSlug}`. */
  slug: string;
  provider: SystrayProvider;
  /** Human-facing name from systray-ai accounts.json alias, else the slug. */
  label: string;
  percent: number | null;
  status: string;
  spend: SystraySpend | null;
  window: { primaryResetAt: number | null; secondaryResetAt: number | null };
  capEtaMinutes: number | null;
}

interface SystraySourceSpec {
  provider: SystrayProvider;
  snapshotPath: string;
  accountsFileName: string;
}

export interface SystrayPanelData {
  stale: boolean;
  ageSeconds: number | null;
  accounts: AccountLimitView[];
}

export interface CreateSystrayAdapterOptions {
  id?: string;
  interval?: number;
  /** Path to systray-ai's Codex HealthStore snapshot JSON file. */
  snapshotPath: string;
  /** Path to systray-ai's Claude HealthStore snapshot; defaults to sibling claude_health_cache.json. */
  claudeSnapshotPath?: string;
  /** When false, only the Codex snapshot is polled (tests / legacy). */
  includeClaude?: boolean;
  /** Snapshot older than this (mtime vs now) is considered stale; no alerts fire. */
  staleAfterS?: number;
  thresholds?: Partial<SystrayThresholds>;
  /** Number of recent samples per account kept for the linear-projection forecast. */
  ringBufferSize?: number;
  now?: () => number;
  /** Injectable for tests; defaults to fs.readFileSync. */
  readFileImpl?: (path: string) => string;
  /** Injectable for tests; defaults to fs.statSync(path).mtimeMs. */
  statImpl?: (path: string) => number;
  /** Injectable for tests; defaults to fs.existsSync. */
  existsImpl?: (path: string) => boolean;
}

const DEFAULT_THRESHOLDS: SystrayThresholds = { percentWarn: 75, etaWarnMinutes: 60 };
const DEFAULT_RING_SIZE = 6;
const DEFAULT_STALE_AFTER_S = 120;
const DEFAULT_INTERVAL_MS = 30_000;

interface RingPoint {
  ts: number;
  percent: number;
}

function accountPercent(snap: SystraySnapshot): number | null {
  const primary = snap.primary_used_pct ?? null;
  const secondary = snap.secondary_used_pct ?? null;
  if (primary === null && secondary === null) return null;
  return Math.max(primary ?? -Infinity, secondary ?? -Infinity);
}

/** Least-squares linear projection of percent-over-time, extrapolated to 100%. */
function forecastEtaMinutes(points: RingPoint[]): number | null {
  if (points.length < 2) return null;
  const n = points.length;
  const meanX = points.reduce((sum, p) => sum + p.ts, 0) / n;
  const meanY = points.reduce((sum, p) => sum + p.percent, 0) / n;
  let numerator = 0;
  let denominator = 0;
  for (const p of points) {
    numerator += (p.ts - meanX) * (p.percent - meanY);
    denominator += (p.ts - meanX) ** 2;
  }
  if (denominator === 0) return null;
  const slopePerMs = numerator / denominator;
  if (slopePerMs <= 0) return null; // flat or falling usage: no cap ETA
  const intercept = meanY - slopePerMs * meanX;
  const etaTs = (100 - intercept) / slopePerMs;
  const latest = points[points.length - 1];
  if (!latest) return null;
  return (etaTs - latest.ts) / 60_000;
}

function accountsRegistryPath(snapshotPath: string, accountsFileName: string): string {
  return join(dirname(snapshotPath), accountsFileName);
}

function loadAccountRegistry(
  readFileImpl: (path: string) => string,
  snapshotPath: string,
  accountsFileName: string,
): SystrayRegistryEntry[] | null {
  try {
    const parsed = JSON.parse(readFileImpl(accountsRegistryPath(snapshotPath, accountsFileName))) as SystrayAccountsFile;
    return Array.isArray(parsed.accounts) ? parsed.accounts : null;
  } catch {
    return null;
  }
}

function providerLabel(provider: SystrayProvider, accountLabel: string): string {
  return `${provider} · ${accountLabel}`;
}

function resolveSources(opts: CreateSystrayAdapterOptions): SystraySourceSpec[] {
  const includeClaude = opts.includeClaude !== false;
  const sources: SystraySourceSpec[] = [
    { provider: "codex", snapshotPath: opts.snapshotPath, accountsFileName: "accounts.json" },
  ];
  if (includeClaude) {
    sources.push({
      provider: "claude",
      snapshotPath: opts.claudeSnapshotPath ?? join(dirname(opts.snapshotPath), "claude_health_cache.json"),
      accountsFileName: "claude_accounts.json",
    });
  }
  return sources;
}

function accountEntries(
  data: SystrayFile,
  registry: SystrayRegistryEntry[] | null,
): Array<{ slug: string; label: string }> {
  if (registry) {
    return registry.map((entry) => ({
      slug: entry.slug,
      label: entry.alias?.trim() || entry.slug,
    }));
  }
  return Object.keys(data).map((slug) => ({ slug, label: slug }));
}

export function createSystrayAdapter(opts: CreateSystrayAdapterOptions): Adapter {
  const id = opts.id ?? "systray-ai";
  const interval = opts.interval ?? DEFAULT_INTERVAL_MS;
  const thresholds: SystrayThresholds = { ...DEFAULT_THRESHOLDS, ...opts.thresholds };
  const ringSize = opts.ringBufferSize ?? DEFAULT_RING_SIZE;
  const staleAfterS = opts.staleAfterS ?? DEFAULT_STALE_AFTER_S;
  const now = opts.now ?? Date.now;
  const readFileImpl = opts.readFileImpl ?? ((path: string) => readFileSync(path, "utf8"));
  const statImpl = opts.statImpl ?? ((path: string) => statSync(path).mtimeMs);
  const existsImpl = opts.existsImpl ?? ((path: string) => existsSync(path));
  const sources = resolveSources(opts);

  const rings = new Map<string, RingPoint[]>();

  function loadSnapshot(snapshotPath: string): { mtimeMs: number; data: SystrayFile } {
    try {
      return { mtimeMs: statImpl(snapshotPath), data: JSON.parse(readFileImpl(snapshotPath)) };
    } catch {
      // atomic-rename races can transiently hide the file or truncate a read; retry once.
      return { mtimeMs: statImpl(snapshotPath), data: JSON.parse(readFileImpl(snapshotPath)) };
    }
  }

  function pushRing(ringKey: string, ts: number, percent: number): RingPoint[] {
    const points = rings.get(ringKey) ?? [];
    points.push({ ts, percent });
    while (points.length > ringSize) points.shift();
    rings.set(ringKey, points);
    return points;
  }

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

      const accounts: AccountLimitView[] = [];
      const items: Item[] = [];
      let ageSeconds = 0;
      let stale = false;

      for (const source of sources) {
        const optional = source.provider !== "codex";
        if (optional && !existsImpl(source.snapshotPath)) continue;

        const { mtimeMs, data } = loadSnapshot(source.snapshotPath);
        const sourceAgeSeconds = (nowMs - mtimeMs) / 1000;
        ageSeconds = Math.max(ageSeconds, sourceAgeSeconds);
        const sourceStale = sourceAgeSeconds > staleAfterS;
        stale = stale || sourceStale;
        const registry = loadAccountRegistry(readFileImpl, source.snapshotPath, source.accountsFileName);

        for (const { slug: accountSlug, label: accountLabel } of accountEntries(data, registry)) {
          const snap = data[accountSlug];
          if (!snap) continue;
          const percent = accountPercent(snap);
          const ringKey = `${source.provider}:${accountSlug}`;
          let capEtaMinutes: number | null = null;
          if (percent !== null) {
            const points = pushRing(ringKey, nowMs, percent);
            capEtaMinutes = forecastEtaMinutes(points);
          }

          const label = providerLabel(source.provider, accountLabel);
          const slug = ringKey;

          accounts.push({
            slug,
            provider: source.provider,
            label,
            percent,
            status: snap.status,
            spend: snap.spend ?? null,
            window: {
              primaryResetAt: snap.primary_reset_at ?? null,
              secondaryResetAt: snap.secondary_reset_at ?? null,
            },
            capEtaMinutes,
          });

          const alertId = `${id}:limit:${slug}`;
          const overThreshold =
            !sourceStale &&
            percent !== null &&
            (percent >= thresholds.percentWarn ||
              (capEtaMinutes !== null && capEtaMinutes < thresholds.etaWarnMinutes));

          if (overThreshold) {
            items.push({
              id: alertId,
              source: id,
              severity: "warn",
              kind: "limit",
              title: `${label} approaching limit`,
              detail: `percent=${percent}% capEtaMinutes=${capEtaMinutes ?? "n/a"}`,
              ts,
              actions: [],
            });
          }
        }
      }

      accounts.sort((left, right) => {
        const providerOrder = left.provider === right.provider ? 0 : left.provider === "codex" ? -1 : 1;
        if (providerOrder !== 0) return providerOrder;
        return left.label.localeCompare(right.label);
      });

      const panel: Panel = {
        id: "limits",
        ts,
        data: { stale, ageSeconds, accounts } satisfies SystrayPanelData,
      };

      return { items, panels: [panel] };
    },
  };
}
