/**
 * SET 89 — sole owner of implementation-artifact map writes (`built`).
 *
 * Every GeneratedFile mutation to the shared artifact goes through this
 * module. Candidate trials snapshot before mutation and restore in `finally`.
 * Adoption is a separate action from the trial.
 */
import type { GeneratedFile } from '../core/types';
import type { ActionLedger } from './actionLedger';

export type ArtifactEdit = {
  editId: string;
  path: string;
  beforeHash: string | null;
  afterHash: string;
  ts: string;
};

export type ArtifactStateSnapshot = {
  files: GeneratedFile[];
  signature: string;
  edits: ArtifactEdit[];
  candidateSeq: number;
  editSeq: number;
};

function pad(n: number, width: number): string {
  return String(n).padStart(width, '0');
}

/** Browser-pure FNV-1a — tier1 bundle forbids `node:crypto`. */
function fnv1aHex(text: string, width = 16): string {
  let h = 2166136261;
  for (let i = 0; i < text.length; i++) {
    h ^= text.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  // Mix a second pass so short identical prefixes diverge more.
  let h2 = 2166136261;
  for (let i = text.length - 1; i >= 0; i--) {
    h2 ^= text.charCodeAt(i);
    h2 = Math.imul(h2, 16777619);
  }
  const a = (h >>> 0).toString(16).padStart(8, '0');
  const b = (h2 >>> 0).toString(16).padStart(8, '0');
  return (a + b).slice(0, width);
}

export function hashContent(content: string): string {
  return fnv1aHex(content, 16);
}

/** Deterministic signature over owned file paths + content hashes. */
export function stateSignature(files: Iterable<GeneratedFile>): string {
  const rows = [...files]
    .map(f => `${f.path}:${hashContent(f.content)}`)
    .sort();
  return fnv1aHex(rows.join('\n'), 24);
}

export class ArtifactState {
  private readonly built = new Map<string, GeneratedFile>();
  private readonly edits: ArtifactEdit[] = [];
  private editSeq = 0;
  private candidateSeq = 0;
  private readonly clock: () => string;
  private readonly ledger: ActionLedger | null;

  constructor(opts?: { clock?: () => string; ledger?: ActionLedger | null }) {
    this.clock = opts?.clock ?? (() => new Date().toISOString());
    this.ledger = opts?.ledger ?? null;
  }

  /** Read-only view of current files. */
  files(): GeneratedFile[] {
    return [...this.built.values()].map(f => ({ ...f }));
  }

  get(path: string): GeneratedFile | undefined {
    const f = this.built.get(path);
    return f ? { ...f } : undefined;
  }

  has(path: string): boolean {
    return this.built.has(path);
  }

  signature(): string {
    return stateSignature(this.built.values());
  }

  allocateCandidateId(): string {
    this.candidateSeq += 1;
    return `C${pad(this.candidateSeq, 6)}`;
  }

  private allocateEditId(): string {
    this.editSeq += 1;
    return `E${pad(this.editSeq, 6)}`;
  }

  private applyOne(file: GeneratedFile): ArtifactEdit {
    const prev = this.built.get(file.path);
    const edit: ArtifactEdit = {
      editId: this.allocateEditId(),
      path: file.path,
      beforeHash: prev ? hashContent(prev.content) : null,
      afterHash: hashContent(file.content),
      ts: this.clock(),
    };
    this.built.set(file.path, { path: file.path, content: file.content });
    this.edits.push(edit);
    return edit;
  }

  /**
   * Commit owned writes. Returns edit IDs applied. Optional action wrapping
   * when a ledger is bound.
   */
  commitWrites(input: {
    files: GeneratedFile[];
    phase: string;
    target: string;
    kind?: string;
    relatedCandidateIds?: string[];
    /** When false, skip ledger action (caller already owns an action). */
    recordAction?: boolean;
  }): { editIds: string[]; signature: string; actionId?: string } {
    const before = this.signature();
    const kind = input.kind ?? 'artifact-write';
    let actionId: string | undefined;
    if (this.ledger && input.recordAction !== false) {
      actionId = this.ledger.startAction({
        kind,
        target: input.target,
        phase: input.phase,
        beforeSignature: before,
        relatedCandidateIds: input.relatedCandidateIds,
      });
    }
    const editIds: string[] = [];
    try {
      for (const f of input.files) {
        editIds.push(this.applyOne(f).editId);
      }
      const after = this.signature();
      if (this.ledger && actionId) {
        this.ledger.finishAction({
          actionId,
          disposition: editIds.length ? 'applied' : 'noop',
          afterSignature: after,
          endEvidence: { editIds, paths: input.files.map(f => f.path) },
        });
      }
      return { editIds, signature: after, actionId };
    } catch (error) {
      if (this.ledger && actionId) {
        this.ledger.finishAction({
          actionId,
          disposition: 'failed',
          failureMode: 'fatal',
          error,
          afterSignature: this.signature(),
        });
      }
      throw error;
    }
  }

  /** Replace entire map from a snapshot (rollback). */
  restoreSnapshot(files: GeneratedFile[]): void {
    this.built.clear();
    for (const f of files) this.built.set(f.path, { path: f.path, content: f.content });
  }

  /**
   * Evaluate a candidate trial: snapshot → mutate → measure → restore in finally.
   * The trial action always terminalizes. Adoption is a SEPARATE call.
   */
  async evaluateCandidateTrial<T>(input: {
    phase: string;
    target: string;
    candidateId: string;
    trialFiles: GeneratedFile[];
    measure: (files: GeneratedFile[]) => Promise<T> | T;
    /** disposition from measure result; default applied for successful measure */
    dispositionOf?: (result: T) => 'applied' | 'rejected' | 'noop' | 'failed';
  }): Promise<{
    actionId?: string;
    candidateId: string;
    result: T;
    beforeSignature: string;
    afterTrialSignature: string;
    restoredSignature: string;
  }> {
    const beforeFiles = this.files();
    const beforeSignature = this.signature();
    let actionId: string | undefined;
    if (this.ledger) {
      actionId = this.ledger.startAction({
        kind: 'candidate-trial',
        target: input.target,
        phase: input.phase,
        beforeSignature,
        relatedCandidateIds: [input.candidateId],
        startEvidence: { paths: input.trialFiles.map(f => f.path) },
      });
    }
    let result!: T;
    let afterTrialSignature = beforeSignature;
    let error: unknown;
    try {
      for (const f of input.trialFiles) this.applyOne(f);
      afterTrialSignature = this.signature();
      result = await input.measure(this.files());
    } catch (e) {
      error = e;
    } finally {
      this.restoreSnapshot(beforeFiles);
    }
    const restoredSignature = this.signature();
    if (this.ledger && actionId) {
      if (error !== undefined) {
        this.ledger.finishAction({
          actionId,
          disposition: 'failed',
          failureMode: 'fatal',
          error,
          afterSignature: restoredSignature,
          endEvidence: { afterTrialSignature, restored: true },
        });
        throw error;
      }
      const disposition = input.dispositionOf?.(result) ?? 'rejected';
      this.ledger.finishAction({
        actionId,
        disposition,
        afterSignature: restoredSignature,
        endEvidence: { afterTrialSignature, restored: true },
      });
    } else if (error !== undefined) {
      throw error;
    }
    return {
      actionId,
      candidateId: input.candidateId,
      result,
      beforeSignature,
      afterTrialSignature,
      restoredSignature,
    };
  }

  /** Adopt a candidate permanently (separate from trial). */
  adoptCandidate(input: {
    phase: string;
    target: string;
    candidateId: string;
    files: GeneratedFile[];
  }): { editIds: string[]; signature: string; actionId?: string } {
    return this.commitWrites({
      files: input.files,
      phase: input.phase,
      target: input.target,
      kind: 'candidate-adopt',
      relatedCandidateIds: [input.candidateId],
    });
  }

  /** Delete a path (rare — used when rolling back an owned file entirely). */
  deletePath(path: string, opts: { phase: string; target: string }): void {
    const before = this.signature();
    let actionId: string | undefined;
    if (this.ledger) {
      actionId = this.ledger.startAction({
        kind: 'artifact-delete',
        target: opts.target,
        phase: opts.phase,
        beforeSignature: before,
      });
    }
    this.built.delete(path);
    if (this.ledger && actionId) {
      this.ledger.finishAction({
        actionId,
        disposition: 'applied',
        afterSignature: this.signature(),
        endEvidence: { path },
      });
    }
  }

  snapshot(): ArtifactStateSnapshot {
    return {
      files: this.files(),
      signature: this.signature(),
      edits: this.edits.map(e => ({ ...e })),
      candidateSeq: this.candidateSeq,
      editSeq: this.editSeq,
    };
  }
}
