// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import type { ConsolidateResult } from "../commands/improve/consolidate";
import type { AkmDistillResult } from "../commands/improve/distill";
import type { AkmExtractResult } from "../commands/improve/extract";
import type {
  ArchivedMemoryCleanupRecord,
  MemoryBeliefStateTransition,
  MemoryConsolidationCandidate,
  MemoryContradictionCandidate,
  MemoryPruneCandidate,
  RelativeDateCandidate,
} from "../commands/improve/memory/memory-improve";
import type { AkmReflectResult } from "../commands/improve/reflect";
import type { DeadUrl } from "../commands/url-checker";
import type { GraphExtractionResult } from "../indexer/graph/graph-extraction";
import type { MemoryInferenceResult } from "../indexer/passes/memory-inference";
import type { StalenessDetectionResult } from "../indexer/passes/staleness-detect";
import { assertNever } from "./assert";

export interface ImproveEligibleRef {
  ref: string;
  reason: "scope-ref" | "scope-type" | "memory-cleanup" | "profile_filtered_all_passes";
  /**
   * Absolute path on disk, pre-resolved from the index at planning time (#591).
   * Avoids repeated serial async DB lookups in the validation and disk-check
   * passes (~500 s on a 9 000-ref stash). Unset for scope-ref entries that
   * bypass `collectEligibleRefs`; consumers fall back to the async lookup.
   */
  filePath?: string;
}

/**
 * The mode discriminator on every {@link ImproveActionResult}. Named so the two
 * audit-counter switches (`computeImproveRunMetrics` in `state-db.ts` and
 * `emitImproveCompletedEvent` in `improve.ts`) and {@link classifyImproveAction}
 * all dispatch over the *same* canonical union — a new variant fails to compile
 * at every consumer rather than silently drifting out of one of them.
 */
export type ImproveActionMode =
  | "reflect"
  | "reflect-failed"
  | "reflect-cooldown"
  | "reflect-skipped"
  | "reflect-guard-rejected"
  | "distill"
  | "distill-skipped"
  | "memory-prune"
  | "memory-inference"
  | "graph-extraction"
  | "error";

/** Coarse audit bucket an {@link ImproveActionMode} contributes to. */
export type ImproveActionClass = "accepted" | "rejected" | "error" | "noop";

/**
 * Map an {@link ImproveActionMode} to its coarse audit bucket. Single source of
 * truth shared by `state-db.ts#computeImproveRunMetrics` and
 * `improve.ts#emitImproveCompletedEvent` so the aggregate accepted/rejected/
 * error counts can never disagree between the persisted `metrics_json` and the
 * emitted `improve_completed` event.
 *
 * Buckets:
 * - `accepted` — a write/content-authoring action succeeded.
 * - `rejected` — the action was deliberately not applied (cooldown, skip,
 *   distill-skip, or a content-policy guard rejection). NOTE: as of the
 *   round-2 health pass `reflect-guard-rejected` is bucketed here. Previously
 *   the `state-db.ts` switch omitted it entirely (no case, no default), so a
 *   guard rejection silently vanished from accepted/rejected/error totals — a
 *   data-integrity miscount. It is a deliberate non-application of the action,
 *   so it belongs with the other "rejected" outcomes.
 * - `error` — the action failed (LLM/runtime error).
 * - `noop` — bookkeeping that is neither a write nor a rejection (memory-prune);
 *   intentionally counted in none of the three numeric buckets.
 *
 * The `default: assertNever(mode)` arm makes any future union variant a
 * compile-time error here, forcing an explicit bucket choice.
 */
export function classifyImproveAction(mode: ImproveActionMode): ImproveActionClass {
  switch (mode) {
    case "reflect":
    case "distill":
    case "memory-inference":
    case "graph-extraction":
      return "accepted";
    case "reflect-cooldown":
    case "reflect-skipped":
    case "distill-skipped":
    case "reflect-guard-rejected":
      return "rejected";
    case "reflect-failed":
    case "error":
      return "error";
    case "memory-prune":
      return "noop";
    default:
      return assertNever(mode);
  }
}

export interface ImproveActionResult {
  ref: string;
  mode: ImproveActionMode;
  result:
    | AkmReflectResult
    | AkmDistillResult
    | MemoryInferenceResult
    | GraphExtractionResult
    | { ok: true; pruned: boolean; reason: MemoryPruneCandidate["reason"] }
    | { ok: true; reason: string }
    | { ok: false; error: string };
}

export interface ImproveMemoryCleanupResult {
  analyzedDerived: number;
  pruneCandidates: MemoryPruneCandidate[];
  contradictionCandidates: MemoryContradictionCandidate[];
  beliefStateTransitions: MemoryBeliefStateTransition[];
  consolidationCandidates: MemoryConsolidationCandidate[];
  relativeDateCandidates?: RelativeDateCandidate[];
  archived?: ArchivedMemoryCleanupRecord[];
  transitionLogPath?: string;
  transitionLogEntries?: number;
  warnings?: string[];
}

export interface AkmImproveResult {
  schemaVersion: 1;
  ok: true;
  scope: {
    mode: "all" | "type" | "ref";
    value?: string;
  };
  dryRun: boolean;
  /**
   * Present when the run did no work because another improve held the lock and
   * `skipIfLocked` was set. The run still exits 0 and records a (non-productive)
   * row so the skip is auditable; `reason` is `"lock-held"`.
   */
  skipped?: { reason: string };
  guidance?: string;
  memorySummary: {
    eligible: number;
    derived: number;
  };
  memoryCleanup?: ImproveMemoryCleanupResult;
  plannedRefs: ImproveEligibleRef[];
  /**
   * Refs the planner considered but excluded because every per-ref pass on
   * the active profile (reflect + distill) would refuse them. Additive
   * field — pre-2026-05-27 these refs went into `plannedRefs` and produced
   * 2× synthetic skip actions per run. See
   * `/tmp/akm-health-investigations/planner-profile-metrics-deep-analysis.md`.
   *
   * Each ref has its `reason` set to `"profile_filtered_all_passes"`. The
   * audit trail is also emitted as a single summary `improve_skipped` event
   * with metadata `{reason, count}` (#592) — one event per ref caused O(n)
   * sequential state.db writes on large stashes.
   * Omitted entirely when no refs were filtered (keeps the envelope tidy
   * for stashes whose profile accepts every indexed type).
   */
  profileFilteredRefs?: ImproveEligibleRef[];
  actions?: ImproveActionResult[];
  validationFailures?: Array<{ ref: string; reason: string }>;
  schemaRepairs?: Array<{
    ref: string;
    reason: string;
    outcome: "queued" | "written" | "skipped" | "error";
    proposalId?: string;
    error?: string;
  }>;
  consolidation?: ConsolidateResult;
  /**
   * Session-extract pass results (one entry per available harness). Present
   * when `profiles.improve.default.processes.extract.enabled` is true (default)
   * and at least one harness reports `isAvailable() === true`.
   */
  extract?: AkmExtractResult[];
  lintSummary?: { fixed: number; flagged: number };
  memoryIndexHealth?: { lineCount: number; overBudget: boolean };
  coverageGaps?: string[];
  evalCasesWritten?: number;
  deadUrls?: DeadUrl[];
  /** Number of reflect calls that had at least one error in the rolling window at call time. */
  reflectsWithErrorContext?: number;
  memoryInference?: MemoryInferenceResult;
  graphExtraction?: GraphExtractionResult;
  /**
   * Wall-clock duration of the memory-inference pass (ms). Surfaced at the
   * top level (not inside `memoryInference`) because both
   * `health.ts#summarizeImproveRuns` (wallTime.byPhase aggregator) and the
   * existing `metrics.memoryInference.durationMs` rollup read it from here.
   * Omitted entirely when the pass did not run.
   */
  memoryInferenceDurationMs?: number;
  /**
   * Wall-clock duration of the graph-extraction pass (ms). Same surfacing
   * convention as `memoryInferenceDurationMs` — top-level so the
   * `wallTime.byPhase.graphExtraction` aggregator in health.ts picks it up.
   * Omitted entirely when the pass did not run.
   */
  graphExtractionDurationMs?: number;
  /** Phase 4A: result of the staleness-detection pass (only present when the feature is enabled and produced telemetry). */
  stalenessDetection?: StalenessDetectionResult;
  /** Number of pending proposals purged because their target ref no longer exists on disk. */
  orphansPurged?: number;
  /**
   * Phase 6B (Advantage D6b): pending proposals archived as expired this run
   * because they aged past `config.archiveRetentionDays`.
   */
  proposalsExpired?: number;
  /** Number of reflect actions that were skipped due to cooldown/dedup signals. */
  reflectCooldownActions?: number;
  /** Number of reflect actions skipped because the asset type is not supported by reflect. */
  reflectSkippedActions?: number;
  /**
   * Number of reflect actions where a downstream content-policy guard
   * (e.g. EXCESSIVE_SHRINKAGE/EXCESSIVE_EXPANSION size rails) blocked an
   * otherwise valid LLM response. NOT counted as LLM failure. See
   * `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1a.
   */
  reflectGuardRejectedActions?: number;
  /**
   * Total proposals auto-promoted by the unified gate across all phases
   * (reflect, extract, distill, consolidate). Populated by summing the
   * `.promoted.length` from every `runAutoAcceptGate` call in the run.
   * Omitted when zero to keep the envelope tidy.
   */
  gateAutoAcceptedCount?: number;
  /**
   * Total proposals that hit the auto-accept gate but failed validation
   * (e.g. truncated description, invalid frontmatter). These are logged as
   * warnings and skipped — they remain in the proposal queue for manual review.
   * Omitted when zero.
   */
  gateAutoAcceptFailedCount?: number;
  /**
   * Triage pre-pass outcome (array lengths from the pre-pass `DrainResult`
   * mapped to counts). Present only when the triage pre-pass actually ran
   * (non-dry-run, triage process enabled, whole-stash / type-scoped run);
   * omitted entirely otherwise to keep the envelope tidy.
   */
  triage?: { promoted: number; rejected: number; deferred: number; skippedByCap: number };
  /**
   * Run identifier minted by the CLI (`buildImproveRunId()`) and threaded
   * through `options.runId`. Surfaced on the result so health/run records and
   * the `{runId}` sync-commit token can read it. Absent for programmatic
   * callers that did not mint one.
   */
  runId?: string;
  /**
   * End-of-run auto-sync outcome for a git-backed primary stash. Present only
   * when sync was attempted (non-dry-run, git-backed stash, sync not disabled).
   * `skipped: true` covers both a no-op `saveGitStash` and a caught failure
   * (a sync failure is non-fatal and never fails the run).
   */
  sync?: { committed: boolean; pushed: boolean; skipped: boolean; reason?: string };
}
