/**
 * The edition-state side of the register (N12 — A1-12).
 *
 * Two jobs, both deterministic:
 *   1. read the handful of facts the register mirrors (series, edition, title,
 *      phase) — refusing to guess when any of them is missing;
 *   2. append the `phaseLog` entry that makes lead time measurable.
 *
 * Why code and not the command layer: the phase log is only useful if it is
 * complete, and "remember to append the right JSON object at all ~17 transitions"
 * is exactly the discipline a model quietly drops mid-pipeline. One CLI call at
 * each transition writes both the log and the register, or neither.
 *
 * The state file is read and written whole — this package touches `phaseLog` and
 * nothing else. `edition-state.json` stays owned by /linkedin:newsletter.
 */

import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";

import type { EditionFacts, EditionStateFile, PhaseLogEntry } from "./types.js";

/** Read the state file. A missing file throws — the register mirrors state, it never invents it. */
export function readEditionState(path: string): EditionStateFile {
  return JSON.parse(readFileSync(path, "utf8")) as EditionStateFile;
}

/** Write the state back, preserving everything this package did not touch. */
export function saveEditionState(path: string, state: EditionStateFile): void {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
}

/**
 * Extract what the register mirrors. Every missing field is an error rather than
 * a default: a row naming the wrong phase is worse than no row, because the
 * whole point is that the operator trusts the list without opening files.
 * The title is the one exception — it is telemetry, not a gate.
 */
export function editionFacts(state: EditionStateFile): EditionFacts {
  const series = state.series?.slug;
  if (typeof series !== "string" || series.trim().length === 0) {
    throw new Error("edition-state is missing series.slug");
  }

  const editionId = state.currentArticle;
  if (typeof editionId !== "string" || editionId.trim().length === 0) {
    throw new Error("edition-state is missing currentArticle");
  }

  const article = state.articles?.[editionId];
  if (!article || typeof article !== "object") {
    throw new Error(`edition-state has no articles entry for currentArticle ${editionId}`);
  }

  const currentPhase = state.currentPhase;
  if (typeof currentPhase !== "string" || currentPhase.trim().length === 0) {
    throw new Error("edition-state is missing currentPhase");
  }

  return { series, editionId, title: typeof article.title === "string" ? article.title : "", currentPhase };
}

export interface AppendPhaseResult {
  state: EditionStateFile;
  /** false iff the same transition was already the newest entry. */
  appended: boolean;
}

/**
 * Append one completed phase to the article's log.
 *
 * Idempotent against an immediately repeated transition (a re-run of the same
 * step), but NOT against a phase that recurs after another one: `/linkedin:pivot`
 * legitimately sends an edition back through cleared gates, and that second pass
 * is real production time — collapsing it would understate the lead time of
 * exactly the editions that cost the most.
 */
export function appendPhase(
  state: EditionStateFile,
  editionId: string,
  phase: string,
  at: string,
): AppendPhaseResult {
  const article = state.articles?.[editionId];
  if (!article || typeof article !== "object") {
    throw new Error(`edition-state has no articles entry for ${editionId}`);
  }

  const log: PhaseLogEntry[] = Array.isArray(article.phaseLog) ? article.phaseLog : [];
  article.phaseLog = log;

  if (log.length > 0 && log[log.length - 1].phase === phase) return { state, appended: false };

  log.push({ phase, completedAt: at });
  return { state, appended: true };
}

/**
 * Derive the series root from the conventional
 * `<serie>/linkedin/edition-state.json` layout, so a transition does not have to
 * repeat a path the state file's own location already carries.
 */
export function seriesRootFromStatePath(statePath: string): string {
  return dirname(dirname(statePath));
}
