/**
 * squash-policy.ts — Pure decision helpers for the squash / rebase-snapshot
 * flow. No I/O, no git, no dotnet — every function here is deterministic so it
 * can be unit-tested in isolation. The side-effecting parts (git tree reads,
 * file delete/restore, `dotnet ef migrations add`) live in squash/execute.ts.
 */

export type SquashMode = 'squash' | 'rebase-snapshot';

/**
 * Minimum number of branch-only migrations required to act.
 *
 *  - `squash` → 2 : consolidating a single migration is a no-op.
 *  - `rebase-snapshot` → 1 : its job is to REPAIR a conflicted snapshot, so it
 *    must regenerate even a single branch-only migration. With zero branch-only
 *    migrations the caller still resets the snapshot to the base (see execute).
 */
export function minBranchOnly(mode: SquashMode): number {
  return mode === 'rebase-snapshot' ? 1 : 2;
}

export interface BehindBaseInput {
  /** How many commits the current branch is behind its reference branch. */
  behindBase: number;
  /** Deliberate user override (escape hatch). */
  allowBehindBase: boolean;
  currentBranch: string;
  /** The reference ref actually used, e.g. "origin/develop". */
  baseRef: string;
  mode: SquashMode;
}

/**
 * Decide whether the operation must refuse because the current branch is behind
 * its reference branch.
 *
 * Squashing a branch that has NOT integrated the latest reference migrations is
 * a data-loss hazard: the flow resets the snapshot to the reference tip and
 * regenerates one migration from the current *code*. If the code lacks the
 * reference's latest entity changes, the regenerated migration tries to REVERT
 * them (drop the tables/columns the reference added) — and that revert lands in
 * production at merge time.
 *
 * Returns a human-readable blocker message when it must refuse, or `null` when
 * it is safe (in sync, or explicitly overridden).
 */
export function behindBaseBlocker(input: BehindBaseInput): string | null {
  const { behindBase, allowBehindBase, currentBranch, baseRef, mode } = input;
  if (behindBase <= 0) return null;
  if (allowBehindBase) return null;
  const verb = mode === 'rebase-snapshot' ? 'rebase-snapshot' : 'squash';
  return (
    `"${currentBranch}" is ${behindBase} commit(s) behind "${baseRef}". ` +
    `Running ${verb} now would re-anchor the snapshot on ${baseRef} and regenerate a migration ` +
    `from code that lacks ${baseRef}'s latest changes — the consolidated migration could DROP ` +
    `what ${baseRef} added (data loss at merge). ` +
    `Sync first (e.g. /gitflow sync, or merge/rebase ${baseRef}), ` +
    `or pass "allowBehindBase": true to override deliberately.`
  );
}

export type SnapshotAction = 'reset-to-base' | 'delete-for-initial-create';

export interface SnapshotActionInput {
  /** Does the reference branch carry a `…ModelSnapshot.cs` for this context? */
  baseHasSnapshot: boolean;
  /** Count of migrations inherited from the reference branch (0 ⇒ empty base). */
  basePreserved: number;
}

/**
 * Decide what the NORMAL squash must do with the model snapshot.
 *
 *  - `reset-to-base` (default): the reference branch is the model's starting
 *    point — restore its `ModelSnapshot.cs` verbatim so the regenerated
 *    migration diffs against it. This is the common case (the branch adds
 *    migrations on top of a context the base already carries).
 *  - `delete-for-initial-create`: the reference branch has NEVER carried this
 *    context (no snapshot AND no inherited migrations — an "empty base", normal
 *    on the FIRST feature branch that introduces a new DbContext). There is
 *    nothing to restore; delete the working-tree snapshot so
 *    `dotnet ef migrations add` regenerates a full InitialCreate. Mirrors the
 *    bruteForce path — but with ZERO casualties (no reference migration exists
 *    to delete).
 *
 * The delete branch is gated on `basePreserved === 0` as a hard invariant: a
 * snapshot that encodes inherited reference migrations is NEVER deleted. A
 * malformed base (migrations present but snapshot absent) falls through to
 * `reset-to-base`, which then fails loudly rather than silently dropping
 * inherited history.
 */
export function decideSnapshotAction(input: SnapshotActionInput): SnapshotAction {
  if (!input.baseHasSnapshot && input.basePreserved === 0) {
    return 'delete-for-initial-create';
  }
  return 'reset-to-base';
}

/**
 * Pure core of the atomic rollback (Part E of the empty-base bug report). Given
 * the migration files currently on disk and the files captured in the pre-run
 * backup (both as cwd-relative, forward-slash paths), return the files that must
 * be DELETED to restore the pre-run state — everything present now that the
 * backup does NOT contain (typically the consolidated migration + its
 * `.Designer.cs` that `dotnet ef migrations add` wrote before the run failed).
 * Files already in the backup are restored by copy, not listed here.
 */
export function rollbackRemovals(currentRelPaths: string[], backedUpRelPaths: string[]): string[] {
  const backedUp = new Set(backedUpRelPaths);
  return currentRelPaths.filter((p) => !backedUp.has(p));
}

/**
 * Idempotently ensure `line` is present in a `.gitignore` body.
 *
 * Returns the new content (with a single trailing newline) when the line must
 * be added, or `null` when it is already present (so the caller can skip the
 * write). Trailing slashes are ignored for the presence check, so
 * `.efcore-squash-backup` and `.efcore-squash-backup/` are treated as equal.
 * Pure string transform — no I/O.
 */
export function withGitignoreLine(existing: string | null, line: string): string | null {
  const norm = (s: string): string => s.trim().replace(/\/+$/, '');
  const target = line.trim();
  const lines = (existing ?? '').split(/\r?\n/);
  if (lines.some((l) => norm(l) !== '' && norm(l) === norm(target))) return null;
  const base = existing && existing.trim().length > 0 ? existing.replace(/\s*$/, '') + '\n' : '';
  return `${base}${target}\n`;
}

/**
 * Build the "your local dev DB history is now stale" advisory for a completed
 * squash. A squash that folds N migrations into a single consolidated one
 * changes their identifiers, so a dev database that has ALREADY applied the old
 * migrations is left with an `__EFMigrationsHistory` that lists IDs no longer
 * present on disk (the physical schema is fine; only the history is stale).
 *
 * The CLI cannot know whether the dev DB applied them, so the message is phrased
 * conditionally. Returns the advisory when at least one assembly actually
 * consolidated (succeeded, produced a new migration, and replaced ≥1 existing
 * migration), or `null` otherwise (no-op / failure). Callers must skip it on a
 * dry run. Pure — no I/O.
 */
export function staleHistoryAdvisory(
  rows: Array<{ success: boolean; newMigrationName: string | null; deletedFiles: string[] }>,
): string | null {
  const consolidated = rows.filter(
    (r) => r.success && r.newMigrationName && r.deletedFiles.length > 0,
  );
  if (consolidated.length === 0) return null;
  const replaced = consolidated.reduce((n, r) => n + r.deletedFiles.length, 0);
  const names = consolidated.map((r) => r.newMigrationName).join(', ');
  const one = consolidated.length === 1;
  return (
    `Dev DB history may now be stale: this squash replaced ${replaced} migration file(s) ` +
    `with ${one ? 'a single consolidated migration' : 'consolidated migrations'} (${names}). ` +
    `If you have already applied the old migrations to your local dev database, its ` +
    `__EFMigrationsHistory still lists the old IDs — recreate the dev DB ` +
    `(drop → reapply Core + the consolidated migration → reseed) to realign. ` +
    `No data loss if the database holds no real data.`
  );
}
