/**
 * The documented TypeSafe usage shapes, made first-class for the interactive command surface.
 *
 * Primitives (question types) are noul / choice / score. On top of them the docs describe four
 * patterns, and the interesting part of each is what *code* does with the answers:
 *
 *   single    one question
 *   fanout    many independent questions in one request (speculative fan-out)
 *   gate      ask, then route on the probability/confidence threshold (confidence-gated routing)
 *   composite several score questions combined with weights (composite scoring)
 *
 * `intent routing` needs no extra machinery here: it is a `choice` question whose answer selects a
 * handler in code, which the single shape already returns.
 */

import { JevError } from "./errors.ts";
import type { JevAnswer } from "./questions.ts";

export type ShapeKind = "single" | "fanout" | "gate" | "composite";

export interface ShapeSpec {
  kind: ShapeKind;
  label: string;
  description: string;
  /** Questions the shape makes sense with. */
  minQuestions: number;
  needsWeights: boolean;
  needsThreshold: boolean;
}

export const SHAPES: Record<ShapeKind, ShapeSpec> = {
  single: {
    kind: "single",
    label: "single — one typed question (noul / choice / score)",
    description: "One judgment, returned with probabilities.",
    minQuestions: 1,
    needsWeights: false,
    needsThreshold: false,
  },
  fanout: {
    kind: "fanout",
    label: "fanout — many independent questions in one request",
    description: "Speculative fan-out: every question sees the same state and they run in parallel in a single call.",
    minQuestions: 2,
    needsWeights: false,
    needsThreshold: false,
  },
  gate: {
    kind: "gate",
    label: "gate — route on a probability/confidence threshold",
    description: "Confidence-gated routing: noul answers gate on the probability, choice/score on confidence.",
    minQuestions: 1,
    needsWeights: false,
    needsThreshold: true,
  },
  composite: {
    kind: "composite",
    label: "composite — weighted score across several score questions",
    description: "Composite scoring: each score answer is normalized over its levels, then combined by weight in code.",
    minQuestions: 2,
    needsWeights: true,
    needsThreshold: false,
  },
};

export interface GateRow {
  id: string;
  /** noul probability, or confidence for choice/score; undefined when the answer reported none. */
  value: number | undefined;
  metric: "noul" | "confidence";
  pass: boolean;
}

/** Which number gates the decision: a noul's probability, otherwise the answer's confidence. */
export function evaluateGate(ids: string[], answers: Record<string, JevAnswer>, threshold: number): GateRow[] {
  return ids.map((id) => {
    const answer = answers[id];
    if (answer === undefined) return { id, value: undefined, metric: "confidence", pass: false };
    const value = answer.type === "noul" ? answer.noul : answer.confidence;
    return {
      id,
      value,
      metric: answer.type === "noul" ? "noul" : "confidence",
      pass: value !== undefined && value >= threshold,
    };
  });
}

export interface CompositeRow {
  id: string;
  /** `score` normalized to 0..1 across its own levels. */
  normalized: number;
  weight: number;
  contribution: number;
}

export interface CompositeResult {
  rows: CompositeRow[];
  total: number;
  totalWeight: number;
}

/**
 * Weighted mean of normalized scores. Questions that are not `score` answers, or that failed
 * conformance, are skipped and reported by the caller.
 */
export function compositeScore(ids: string[], answers: Record<string, JevAnswer>, weights: number[]): CompositeResult {
  const rows: CompositeRow[] = [];
  ids.forEach((id, index) => {
    const answer = answers[id];
    if (answer === undefined || answer.type !== "score") return;
    const levels = Object.keys(answer.legend).length;
    const normalized = levels <= 1 ? 0 : answer.score / (levels - 1);
    const weight = weights[index] ?? 1;
    rows.push({ id, normalized, weight, contribution: normalized * weight });
  });
  const totalWeight = rows.reduce((sum, row) => sum + row.weight, 0);
  const total = totalWeight === 0 ? 0 : rows.reduce((sum, row) => sum + row.contribution, 0) / totalWeight;
  return { rows, total, totalWeight };
}

/** Parse `1,2,0.5` into weights (missing entries default to 1). */
export function parseWeights(raw: string, count: number): number[] {
  const parts = raw
    .split(/[,\s]+/)
    .map((part) => part.trim())
    .filter((part) => part !== "");
  if (parts.length === 0) return Array.from({ length: count }, () => 1);
  const weights = parts.map((part) => Number(part));
  if (weights.some((value) => !Number.isFinite(value) || value < 0)) {
    throw new JevError(`Weights must be non-negative numbers, got "${raw}".`);
  }
  while (weights.length < count) weights.push(1);
  return weights.slice(0, count);
}
