import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import type {
  AlwaysOnCurrentWorkspaceRef,
  AlwaysOnDiscoveryOutcome,
  AlwaysOnDiscoveryState,
  WorkspaceStrategyId,
} from "../protocol/types.js";
import type { AlwaysOnPaths } from "./AlwaysOnPaths.js";

export function getDayKey(date: Date): string {
  return date.toISOString().slice(0, 10);
}

export function defaultDiscoveryState(now: Date): AlwaysOnDiscoveryState {
  return {
    schemaVersion: 1,
    todayKey: getDayKey(now),
    todayRunCount: 0,
    consecutiveFailures: 0,
  };
}

export class DiscoveryStateStore {
  constructor(private readonly paths: AlwaysOnPaths) {}

  async read(now: Date): Promise<AlwaysOnDiscoveryState> {
    let raw: string;
    try {
      raw = await readFile(this.paths.stateFile, "utf-8");
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === "ENOENT") {
        return defaultDiscoveryState(now);
      }
      throw error;
    }
    let parsed: unknown;
    try {
      parsed = JSON.parse(raw);
    } catch {
      return defaultDiscoveryState(now);
    }
    return resetDailyBudgetIfNeeded(normalizeState(parsed, now), now);
  }

  async write(state: AlwaysOnDiscoveryState): Promise<void> {
    await mkdir(dirname(this.paths.stateFile), { recursive: true });
    await writeFile(this.paths.stateFile, JSON.stringify(state, null, 2), "utf-8");
  }

  async markFireStarted(runId: string, now: Date): Promise<AlwaysOnDiscoveryState> {
    const current = await this.read(now);
    const next: AlwaysOnDiscoveryState = {
      ...current,
      lastFireStartedAt: now.toISOString(),
      lastRunId: runId,
      todayKey: getDayKey(now),
      todayRunCount: current.todayRunCount + 1,
    };
    await this.write(next);
    return next;
  }

  async markFireCompleted(input: {
    outcome: AlwaysOnDiscoveryOutcome;
    runId: string;
    planId?: string;
    now: Date;
  }): Promise<AlwaysOnDiscoveryState> {
    const current = await this.read(input.now);
    const next: AlwaysOnDiscoveryState = {
      ...current,
      lastFireCompletedAt: input.now.toISOString(),
      lastFireOutcome: input.outcome,
      lastRunId: input.runId,
      lastPlanId: input.planId,
      consecutiveFailures: input.outcome === "failed" ? current.consecutiveFailures + 1 : 0,
    };
    await this.write(next);
    return next;
  }

  async setActiveWorkCycleId(cycleId: string, now: Date): Promise<AlwaysOnDiscoveryState> {
    const current = await this.read(now);
    const next: AlwaysOnDiscoveryState = {
      ...current,
      activeWorkCycleId: cycleId,
    };
    delete next.currentWorkspace;
    await this.write(next);
    return next;
  }

  async clearActiveWorkCycleId(now: Date): Promise<AlwaysOnDiscoveryState> {
    const current = await this.read(now);
    if (!current.activeWorkCycleId) {
      return current;
    }
    const next: AlwaysOnDiscoveryState = { ...current };
    delete next.activeWorkCycleId;
    await this.write(next);
    return next;
  }

  async setDormant(now: Date): Promise<AlwaysOnDiscoveryState> {
    const current = await this.read(now);
    const next: AlwaysOnDiscoveryState = {
      ...current,
      dormant: {
        since: now.toISOString(),
        lastBaselineAt: now.toISOString(),
      },
    };
    await this.write(next);
    return next;
  }

  async clearDormant(now: Date): Promise<AlwaysOnDiscoveryState> {
    const current = await this.read(now);
    if (!current.dormant) {
      return current;
    }
    const next: AlwaysOnDiscoveryState = { ...current };
    delete next.dormant;
    await this.write(next);
    return next;
  }
}

function normalizeState(value: unknown, now: Date): AlwaysOnDiscoveryState {
  if (!value || typeof value !== "object") {
    return defaultDiscoveryState(now);
  }
  const candidate = value as Partial<AlwaysOnDiscoveryState> & Record<string, unknown>;
  if (candidate.schemaVersion !== 1) {
    return defaultDiscoveryState(now);
  }
  return {
    schemaVersion: 1,
    lastFireStartedAt: typeof candidate.lastFireStartedAt === "string" ? candidate.lastFireStartedAt : undefined,
    lastFireCompletedAt:
      typeof candidate.lastFireCompletedAt === "string" ? candidate.lastFireCompletedAt : undefined,
    lastFireOutcome: normalizeOutcome(candidate.lastFireOutcome),
    lastPlanId: typeof candidate.lastPlanId === "string" ? candidate.lastPlanId : undefined,
    lastRunId: typeof candidate.lastRunId === "string" ? candidate.lastRunId : undefined,
    todayKey: typeof candidate.todayKey === "string" ? candidate.todayKey : getDayKey(now),
    todayRunCount:
      typeof candidate.todayRunCount === "number" && candidate.todayRunCount >= 0
        ? candidate.todayRunCount
        : 0,
    consecutiveFailures:
      typeof candidate.consecutiveFailures === "number" && candidate.consecutiveFailures >= 0
        ? candidate.consecutiveFailures
        : 0,
    dormant: normalizeDormant(candidate.dormant),
    activeWorkCycleId: typeof candidate.activeWorkCycleId === "string" ? candidate.activeWorkCycleId : undefined,
    currentWorkspace: normalizeCurrentWorkspace(candidate.currentWorkspace),
  };
}

function normalizeCurrentWorkspace(value: unknown): AlwaysOnCurrentWorkspaceRef | undefined {
  if (!value || typeof value !== "object") return undefined;
  const candidate = value as Record<string, unknown>;
  const runId = typeof candidate.runId === "string" ? candidate.runId : undefined;
  const cwd = typeof candidate.cwd === "string" ? candidate.cwd : undefined;
  const strategy = normalizeWorkspaceStrategy(candidate.strategy);
  if (!runId || !cwd || !strategy) return undefined;
  const rawMeta = candidate.metadata;
  const metadata: Record<string, string> = {};
  if (rawMeta && typeof rawMeta === "object" && !Array.isArray(rawMeta)) {
    for (const [key, val] of Object.entries(rawMeta as Record<string, unknown>)) {
      if (typeof val === "string") metadata[key] = val;
    }
  }
  return { runId, cwd, strategy, metadata };
}

function normalizeWorkspaceStrategy(value: unknown): WorkspaceStrategyId | undefined {
  if (value === "git-worktree" || value === "snapshot-copy") return value;
  return undefined;
}

function normalizeOutcome(value: unknown): AlwaysOnDiscoveryState["lastFireOutcome"] {
  if (
    value === "executed" ||
    value === "no_plan" ||
    value === "failed" ||
    value === "aborted"
  ) {
    return value;
  }
  return undefined;
}

function normalizeDormant(value: unknown): AlwaysOnDiscoveryState["dormant"] {
  if (!value || typeof value !== "object") {
    return undefined;
  }
  const dormant = value as Record<string, unknown>;
  if (typeof dormant.since !== "string" || typeof dormant.lastBaselineAt !== "string") {
    return undefined;
  }
  return {
    since: dormant.since,
    lastBaselineAt: dormant.lastBaselineAt,
    lastChangeAt: typeof dormant.lastChangeAt === "string" ? dormant.lastChangeAt : undefined,
  };
}

function resetDailyBudgetIfNeeded(
  state: AlwaysOnDiscoveryState,
  now: Date,
): AlwaysOnDiscoveryState {
  const today = getDayKey(now);
  if (state.todayKey === today) {
    return state;
  }
  return { ...state, todayKey: today, todayRunCount: 0 };
}