/**
 * The editions register (N12 — A1-11 / A1-12).
 *
 * One row per edition in production, so the week's work-in-progress is readable
 * without opening a single `edition-state.json`, and lead time (first transition
 * → completion) is measured rather than assumed.
 *
 * Pure where it matters: every mutation takes `now` as an argument, exactly like
 * the distillate takes `lockedAt`. No clock in the core means no run-to-run
 * variance in the tests and no hidden timezone behaviour in the data.
 *
 * Mirror discipline: nothing here is authoritative. Rows are derived from
 * edition-state files at phase transitions; `startedAt` is the only value that
 * cannot be recovered by re-running one, which is why it never moves.
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { homedir } from "node:os";

import { REGISTER_SCHEMA_VERSION } from "./types.js";
import type { EditionRegister, EditionRow, EditionStatus, UpsertInput } from "./types.js";

export { REGISTER_SCHEMA_VERSION } from "./types.js";

const MS_PER_DAY = 86_400_000;

export function emptyRegister(): EditionRegister {
  return { schemaVersion: REGISTER_SCHEMA_VERSION, editions: [] };
}

/** Read the register; a missing file is an empty one (the first edition is not an error). */
export function loadRegister(path: string): EditionRegister {
  if (!existsSync(path)) return emptyRegister();
  const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<EditionRegister>;
  return {
    schemaVersion: parsed.schemaVersion ?? REGISTER_SCHEMA_VERSION,
    editions: Array.isArray(parsed.editions) ? parsed.editions : [],
  };
}

/** Write as pretty JSON with a trailing newline, creating the parent dir if needed. */
export function saveRegister(path: string, register: EditionRegister): void {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, JSON.stringify(register, null, 2) + "\n", "utf8");
}

export interface UpsertResult {
  register: EditionRegister;
  row: EditionRow;
  /** true iff the edition had no row yet. */
  created: boolean;
}

/**
 * Mirror one phase transition into the register.
 *
 * Omitted optionals are preserved: a transition reports the phase it just
 * completed, and it must not blank the slot or the title just because it had no
 * reason to mention them. Re-upserting a completed edition REACTIVATES its row —
 * `/linkedin:pivot` re-opens an edition, and the lead-time clock it started
 * should keep running rather than fork a second row.
 */
export function upsertEdition(register: EditionRegister, input: UpsertInput, now: string): UpsertResult {
  const at = register.editions.findIndex(
    (e) => e.series === input.series && e.editionId === input.editionId,
  );

  if (at < 0) {
    const row: EditionRow = {
      series: input.series,
      editionId: input.editionId,
      title: input.title ?? "",
      path: input.path,
      currentPhase: input.currentPhase,
      nextAction: input.nextAction ?? null,
      slot: input.slot ?? null,
      startedAt: now,
      updatedAt: now,
      completedAt: null,
      status: "active",
    };
    register.editions.push(row);
    return { register, row, created: true };
  }

  const existing = register.editions[at];
  const row: EditionRow = {
    ...existing,
    title: input.title ?? existing.title,
    path: input.path,
    currentPhase: input.currentPhase,
    nextAction: input.nextAction ?? existing.nextAction,
    slot: input.slot ?? existing.slot,
    updatedAt: now,
    completedAt: null,
    status: "active",
  };
  register.editions[at] = row;
  return { register, row, created: false };
}

export interface CompleteResult {
  register: EditionRegister;
  /** null iff no row matched — completing an edition that was never registered is a caller error. */
  row: EditionRow | null;
}

/**
 * Close an edition. Idempotent by design: a second call keeps the FIRST
 * `completedAt`, so re-running Step 10 cannot quietly stretch the measured lead
 * time of an edition that was already done.
 */
export function completeEdition(
  register: EditionRegister,
  key: { series: string; editionId: string },
  now: string,
): CompleteResult {
  const at = register.editions.findIndex((e) => e.series === key.series && e.editionId === key.editionId);
  if (at < 0) return { register, row: null };

  const existing = register.editions[at];
  if (existing.status === "complete") return { register, row: existing };

  const row: EditionRow = { ...existing, status: "complete", completedAt: now, updatedAt: now };
  register.editions[at] = row;
  return { register, row };
}

export interface ListOptions {
  /** Include finished editions — the history behind the in-flight view. */
  includeComplete?: boolean;
}

const STATUS_ORDER: Record<EditionStatus, number> = { active: 0, complete: 1 };

/** Deterministic order: in flight first, then by series, then by edition id. */
export function listEditions(register: EditionRegister, options: ListOptions = {}): EditionRow[] {
  const rows = options.includeComplete
    ? [...register.editions]
    : register.editions.filter((e) => e.status === "active");

  return rows.sort(
    (a, b) =>
      STATUS_ORDER[a.status] - STATUS_ORDER[b.status] ||
      a.series.localeCompare(b.series) ||
      a.editionId.localeCompare(b.editionId),
  );
}

/**
 * Days between two ISO stamps, to two decimals — the lead-time primitive.
 * Sub-day precision matters: at two editions a week, "1 day" and "0.5 days" are
 * a different production reality. An unparseable stamp yields null, never NaN.
 */
export function elapsedDays(from: string, to: string): number | null {
  const a = Date.parse(from);
  const b = Date.parse(to);
  if (Number.isNaN(a) || Number.isNaN(b)) return null;
  return Math.round(((b - a) / MS_PER_DAY) * 100) / 100;
}

/**
 * Default register path under the per-user data dir (M0 data-path convention),
 * so the WIP view survives plugin upgrades/reinstalls. `LINKEDIN_STUDIO_DATA`
 * overrides the root; otherwise `~/.claude/linkedin-studio`.
 */
export function defaultRegisterPath(): string {
  const root = process.env.LINKEDIN_STUDIO_DATA ?? join(homedir(), ".claude", "linkedin-studio");
  return join(root, "editions", "register.json");
}
