/**
 * src/constitution/signal.ts
 *
 * Publishes soft constitutional scorer results to Brain episodic memory.
 * Decouples the scoring pipeline from Brain I/O — purely an adapter.
 *
 * @module constitution/signal
 */

import type { EpisodicMemoryPort } from "../ports/brain.js";
import type { AxiomId, CycleSnapshot } from "./types.js";

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/**
 * Minimal Brain port required by publishSignal.
 * Requires only the episodic tier (Tier 2).
 */
export interface SignalBrainPort {
  episodic?: EpisodicMemoryPort;
}

// ---------------------------------------------------------------------------
// publishSignal
// ---------------------------------------------------------------------------

/**
 * Publish a constitutional soft-score signal to Brain episodic memory.
 *
 * Tags: ["constitution_signal", axiomId]
 *
 * @param brainPort  - Host-injected BrainPort. If `episodic` is absent, no-ops silently.
 * @param cycle      - The cycle that was scored.
 * @param axiomId    - The axiom that produced the score.
 * @param score      - Normalised score in [0, 1].
 * @param rationale  - Human-readable scoring rationale for audit.
 */
export async function publishSignal(
  brainPort: SignalBrainPort,
  cycle: CycleSnapshot,
  axiomId: AxiomId,
  score: number,
  rationale: string,
): Promise<void> {
  if (!brainPort.episodic) return;

  const event = "constitution_signal";
  const metadata: Record<string, unknown> = {
    axiom: axiomId,
    score,
    rationale,
    runId: cycle.runId,
    rootHash: cycle.rootHash,
    tags: ["constitution_signal", axiomId],
    budgetUsdMax: cycle.budgetUsdMax,
    budgetUsdSpent: cycle.budgetUsdSpent,
  };

  if (cycle.parentRunId !== undefined) {
    metadata.parentRunId = cycle.parentRunId;
  }

  if (cycle.scope_id !== undefined) {
    metadata.scope_id = cycle.scope_id;
  }

  await brainPort.episodic.record(
    // agentId: use runId as the owning agent identifier for constitution signals
    `constitution:${axiomId}`,
    cycle.runId,
    event,
    metadata,
  );
}

// ---------------------------------------------------------------------------
// publishAllSignals — convenience for scoring all axioms in one call
// ---------------------------------------------------------------------------

export interface AxiomScoreEntry {
  axiomId: AxiomId;
  score: number;
  rationale: string;
}

/**
 * Publish all axiom scores for a single cycle to Brain episodic.
 * Fires all records in parallel; individual failures are re-thrown as an
 * AggregateError to avoid silent drops.
 */
export async function publishAllSignals(
  brainPort: SignalBrainPort,
  cycle: CycleSnapshot,
  scores: AxiomScoreEntry[],
): Promise<void> {
  const results = await Promise.allSettled(
    scores.map(({ axiomId, score, rationale }) =>
      publishSignal(brainPort, cycle, axiomId, score, rationale),
    ),
  );

  const failures = results
    .filter((r): r is PromiseRejectedResult => r.status === "rejected")
    .map((r) => r.reason as Error);

  if (failures.length > 0) {
    throw new AggregateError(
      failures,
      `publishAllSignals: ${failures.length} Brain episodic write(s) failed`,
    );
  }
}
