// Phase 2 Task 2.5 of the principles-as-wiki-kind plan: principle_decide
// core math. Pure module — no I/O, no logging side effects. The Phase 2
// Task 2.7 MCP handler is responsible for retrieving principles and
// embeddings, then calling these primitives.
//
// Spec: docs/superpowers/specs/2026-05-12-principles-as-wiki-kind-design.md §11
// Plan: docs/superpowers/plans/2026-05-12-principles-as-wiki-kind.md (Phase 2 Tasks 2.5)
//
// Decision algorithm (spec §11.2):
//   1. Score each option against each principle (structured if both have
//      compatible dimension data; otherwise semantic fallback via embedding
//      cosine).
//   2. Composite per option = Σ (principle.weight × alignment).
//   3. Caller picks argmax; this module returns the full contribution
//      ledger so callers can render the why, not just the what.

// ─── Public types ───────────────────────────────────────────────────────────

export type DecisionPrinciple = {
  id: string;
  name: string;
  /** commandment | core | contextual (loose type for storage round-trips). */
  tier: string;
  /** Effective decision weight (from tier default or override). */
  weight: number;
  /** Signed map of dimension key → weight in [-1, 1]. Empty triggers semantic fallback. */
  dimensionVector: Record<string, number>;
  /** Optional embedding of principleDirection for semantic fallback. */
  directionEmbedding?: number[];
};

export type DecisionOption = {
  id: string;
  description: string;
  /** Map of dimension key → 0..1 score. Only structurally compared against principles whose dimensionVector overlaps. */
  features: Record<string, number>;
  /** Optional embedding of the option description for semantic fallback. */
  embedding?: number[];
};

export type AlignmentMode = "structured" | "semantic";

export type AlignmentResult = {
  alignment: number;
  mode: AlignmentMode;
  /** Dimensions the principle cared about but the option did not score. */
  missingDimensions: string[];
};

export type PrincipleContribution = {
  principleId: string;
  principleName: string;
  tier: string;
  weight: number;
  mode: AlignmentMode;
  alignment: number;
  /** weight × alignment — the row's contribution to the composite. */
  contribution: number;
  /** Dimensions present in the principle's vector but absent from the option (structured mode only). */
  missingDimensions?: string[];
};

export type DecisionOptionScore = {
  optionId: string;
  composite: number;
  contributions: PrincipleContribution[];
};

// ─── Structured alignment ───────────────────────────────────────────────────

/**
 * Normalized dot product over the principle's dimension axes:
 *
 *   alignment = Σ option[dim] × vector[dim] / Σ |vector[dim]|
 *
 * Missing option dimensions count as 0 (no contribution along that axis)
 * and are reported in `missingDimensions` so the caller can warn about
 * partial coverage. Returns alignment = 0 when the principle has no
 * dimensions at all — callers detect this and fall back to semantic mode.
 */
export function computeStructuredAlignment(
  option: DecisionOption,
  principle: DecisionPrinciple,
): AlignmentResult {
  const dims = Object.keys(principle.dimensionVector);
  if (dims.length === 0) {
    return { alignment: 0, mode: "structured", missingDimensions: [] };
  }

  let numerator = 0;
  let denominator = 0;
  const missingDimensions: string[] = [];
  for (const dim of dims) {
    const v = principle.dimensionVector[dim];
    const f = option.features[dim];
    denominator += Math.abs(v);
    if (typeof f === "number") {
      numerator += f * v;
    } else {
      missingDimensions.push(dim);
    }
  }

  const alignment = denominator === 0 ? 0 : numerator / denominator;
  return { alignment, mode: "structured", missingDimensions };
}

// ─── Semantic alignment fallback ────────────────────────────────────────────

/**
 * Cosine similarity between the option embedding and the principle's
 * `directionEmbedding`. Used when the principle has no `dimensionVector`
 * (or the caller has no features to score). Returns 0 when either side
 * is missing or has zero norm — the caller decides whether a 0 alignment
 * means "no signal" vs. "actively neutral".
 */
export function computeSemanticAlignment(
  option: DecisionOption,
  principle: DecisionPrinciple,
): AlignmentResult {
  const a = option.embedding;
  const b = principle.directionEmbedding;
  if (!a || !b || a.length !== b.length || a.length === 0) {
    return { alignment: 0, mode: "semantic", missingDimensions: [] };
  }
  let dot = 0;
  let normA = 0;
  let normB = 0;
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  if (normA === 0 || normB === 0) {
    return { alignment: 0, mode: "semantic", missingDimensions: [] };
  }
  return {
    alignment: dot / (Math.sqrt(normA) * Math.sqrt(normB)),
    mode: "semantic",
    missingDimensions: [],
  };
}

// ─── Composite scoring + contribution ledger ────────────────────────────────

/**
 * For each option, compute the composite decision score across all
 * principles and return the contribution ledger.
 *
 * Selection of structured vs. semantic alignment is per-principle:
 * - If `principle.dimensionVector` has at least one entry → structured.
 * - Otherwise → semantic (uses both embeddings; zero if either is missing).
 *
 * Tasks 2.6 (guardrails: tie-margin, commandment-conflict, semantic-
 * fallback coverage) consume this output downstream.
 */
// ─── Guardrails + advisory recommendation (Task 2.6) ───────────────────────

export type DecisionFlags = {
  /** The tie-margin threshold used for this decision (echo for inspectability). */
  tieMargin: number;
  /** Fraction of contributions that fell back to semantic alignment. */
  semanticFallbackRatio: number;
  /** "weak" when semanticFallbackRatio > semanticFallbackWarnRatio. */
  structuredCoverage: "strong" | "weak";
  /** True when any commandment contributed strongly negatively to the top option. */
  commandmentConflict: boolean;
  /** ids of commandments triggering the commandmentConflict flag. */
  commandmentConflictPrinciples: string[];
};

export type DecisionRecommendation = {
  optionId: string;
  composite: number;
  /** Composite of winner − composite of runner-up (0 when only one option). */
  margin: number;
  /** "low" when margin < tieMargin OR no principles applied. */
  confidence: "high" | "low";
};

export type DecisionResult = {
  recommendation: DecisionRecommendation | null;
  scores: DecisionOptionScore[];
  flags: DecisionFlags;
  /** Human-readable summary naming the winner and top contributors. */
  reasoning: string;
};

export type DecideConfig = {
  tieMargin?: number;
  semanticFallbackWarnRatio?: number;
  /**
   * A commandment contributes "strongly negatively" to the top option when
   * its contribution is below -commandmentConflictThreshold (default 0.5).
   * Tuned to catch values like -1.0 (full opposition under weight=1.0)
   * without flagging routine small negatives.
   */
  commandmentConflictThreshold?: number;
};

const DEFAULT_TIE_MARGIN = 0.2;
const DEFAULT_SEMANTIC_FALLBACK_WARN_RATIO = 0.4;
const DEFAULT_COMMANDMENT_CONFLICT_THRESHOLD = 0.5;

/**
 * Top-level advisory decision call. Wraps buildOptionScores with the
 * three guardrails from spec section 11.3 and a human-readable reasoning
 * string. Returns a `recommendation: null` shape when no principles
 * apply or no options are supplied so callers can surface "no signal"
 * to the user rather than picking arbitrarily.
 */
export function decide(
  options: DecisionOption[],
  principles: DecisionPrinciple[],
  config: DecideConfig = {},
): DecisionResult {
  const tieMargin = config.tieMargin ?? DEFAULT_TIE_MARGIN;
  const semanticWarnRatio =
    config.semanticFallbackWarnRatio ?? DEFAULT_SEMANTIC_FALLBACK_WARN_RATIO;
  const commandmentConflictThreshold =
    config.commandmentConflictThreshold ??
    DEFAULT_COMMANDMENT_CONFLICT_THRESHOLD;

  const scores = buildOptionScores(options, principles);

  // No options or no principles → no signal; caller surfaces a "no
  // recommendation" message rather than picking arbitrarily.
  if (scores.length === 0 || principles.length === 0) {
    const flags: DecisionFlags = {
      tieMargin,
      semanticFallbackRatio: 0,
      structuredCoverage: "strong",
      commandmentConflict: false,
      commandmentConflictPrinciples: [],
    };
    const reasoning =
      principles.length === 0
        ? "No applicable principles to evaluate. The decision needs human judgment instead of advisory math."
        : "No options supplied. Nothing to score.";
    return { recommendation: null, scores, flags, reasoning };
  }

  // Rank by composite descending.
  const ranked = [...scores].sort((a, b) => b.composite - a.composite);
  const winner = ranked[0];
  const runnerUpComposite = ranked[1]?.composite ?? winner.composite;
  const margin = winner.composite - runnerUpComposite;
  const confidence: "high" | "low" = margin < tieMargin ? "low" : "high";

  // Coverage: ratio of semantic-mode contributions across all option × principle pairs.
  const totalContribs = scores.reduce((sum, s) => sum + s.contributions.length, 0);
  const semanticContribs = scores.reduce(
    (sum, s) => sum + s.contributions.filter((c) => c.mode === "semantic").length,
    0,
  );
  const semanticFallbackRatio = totalContribs === 0 ? 0 : semanticContribs / totalContribs;
  const structuredCoverage: "strong" | "weak" =
    semanticFallbackRatio > semanticWarnRatio ? "weak" : "strong";

  // Commandment conflict against the top option.
  const conflictingPrinciples = winner.contributions
    .filter(
      (c) =>
        c.tier === "commandment" &&
        c.contribution < -commandmentConflictThreshold,
    )
    .map((c) => c.principleId);

  const flags: DecisionFlags = {
    tieMargin,
    semanticFallbackRatio,
    structuredCoverage,
    commandmentConflict: conflictingPrinciples.length > 0,
    commandmentConflictPrinciples: conflictingPrinciples,
  };

  // Reasoning: name the winner, the top two contributing principles, and
  // any quality warnings. Kept short — the contribution ledger carries
  // the full inspectable breakdown.
  const topContribs = [...winner.contributions]
    .sort((a, b) => b.contribution - a.contribution)
    .slice(0, 2)
    .map((c) => c.principleName);
  const parts: string[] = [];
  parts.push(
    `Recommends ${winner.optionId} (composite ${winner.composite.toFixed(3)}, margin ${margin.toFixed(3)}).`,
  );
  if (topContribs.length > 0) {
    parts.push(`Strongest contributors: ${topContribs.join(", ")}.`);
  }
  if (confidence === "low") {
    parts.push(
      `Margin is below tieMargin (${tieMargin}); the call is close — recommend human review before committing.`,
    );
  }
  if (flags.commandmentConflict) {
    parts.push(
      `Commandment conflict: ${conflictingPrinciples.join(", ")} oppose the recommended option. Re-check whether the tension is intentional.`,
    );
  }
  if (flags.structuredCoverage === "weak") {
    parts.push(
      `Structured coverage is weak (${Math.round(semanticFallbackRatio * 100)}% semantic fallback). Consider supplying explicit dimension features on the options.`,
    );
  }
  const reasoning = parts.join(" ");

  return {
    recommendation: {
      optionId: winner.optionId,
      composite: winner.composite,
      margin,
      confidence,
    },
    scores,
    flags,
    reasoning,
  };
}

export function buildOptionScores(
  options: DecisionOption[],
  principles: DecisionPrinciple[],
): DecisionOptionScore[] {
  return options.map((option) => {
    const contributions: PrincipleContribution[] = principles.map((p) => {
      const useStructured = Object.keys(p.dimensionVector).length > 0;
      const aln = useStructured
        ? computeStructuredAlignment(option, p)
        : computeSemanticAlignment(option, p);
      const contribution = p.weight * aln.alignment;
      const row: PrincipleContribution = {
        principleId: p.id,
        principleName: p.name,
        tier: p.tier,
        weight: p.weight,
        mode: aln.mode,
        alignment: aln.alignment,
        contribution,
      };
      if (aln.mode === "structured" && aln.missingDimensions.length > 0) {
        row.missingDimensions = aln.missingDimensions;
      }
      return row;
    });

    const composite = contributions.reduce((sum, c) => sum + c.contribution, 0);
    return { optionId: option.id, composite, contributions };
  });
}
