/**
 * engine-types.ts — Engine-polymorphic type contracts.
 *
 * LEAF NODE: This file must have ZERO imports from any SF module.
 * Only `node:` imports are permitted. All engine/policy interfaces
 * depend on these types; nothing here depends on SF internals.
 */

const REQUIRED_PDD_FIELDS = [
	"purpose",
	"consumer",
	"contract",
	"failureBoundary",
	"evidence",
	"invariants",
];

const RISK_LEVELS = ["low", "medium", "high", "critical"];
const BLAST_RADIUS_LEVELS = ["unit", "slice", "milestone", "project", "org"];
const CUSTOMER_IMPACT_LEVELS = [
	"none",
	"internal",
	"customer-visible",
	"customer-critical",
];
const GATE_OUTCOMES = ["pass", "fail", "retry", "manual-attention"];

/**
 * UOK gate type enum per ADR-0075 line 20.
 *
 * Purpose: canonical list every gate.type field must match. Used by
 * register() validation in uok/gate-runner.js.
 */
export const UOK_GATE_TYPES = Object.freeze([
	"security",
	"policy",
	"verification",
	"learning",
	"chaos",
]);

export function isValidGateType(value) {
	return typeof value === "string" && UOK_GATE_TYPES.includes(value);
}
const FAILURE_CLASSES = [
	"policy",
	"verification",
	"execution",
	"artifact",
	"git",
	"timeout",
	"input",
	"closeout",
	"manual-attention",
	"unknown",
];

const RISK_SCORES = { low: 0.85, medium: 0.55, high: 0.25, critical: 0 };
const BLAST_RADIUS_SCORES = {
	unit: 1,
	slice: 0.75,
	milestone: 0.5,
	project: 0.25,
	org: 0,
};
const CUSTOMER_IMPACT_SCORES = {
	none: 1,
	internal: 0.75,
	"customer-visible": 0.35,
	"customer-critical": 0,
};

/**
 * @typedef {object} PDDFields
 * @property {string} purpose
 * @property {string} consumer
 * @property {string} contract
 * @property {string} failureBoundary
 * @property {string|object} evidence
 * @property {string[]|string} [nonGoals]
 * @property {string[]|string} invariants
 * @property {string[]|string} [assumptions]
 */

/**
 * Validate PDD fields before a plan or implementation can claim purpose coverage.
 *
 * Purpose: protect spec-first flow from running units whose consumer, contract,
 * evidence, or invariant boundaries are absent.
 *
 * Consumer: SF planning and gate code that needs a leaf-node PDD contract check.
 *
 * @param {unknown} input
 * @returns {{ ok: boolean, missing: string[], errors: string[] }}
 */
export function validatePDDFields(input) {
	const missing = [];
	const errors = [];

	if (!isRecord(input)) {
		return {
			ok: false,
			missing: [...REQUIRED_PDD_FIELDS],
			errors: ["input must be an object"],
		};
	}

	for (const field of REQUIRED_PDD_FIELDS) {
		if (!hasNonEmptyValue(input[field])) {
			missing.push(field);
		}
	}

	for (const field of ["purpose", "consumer", "contract", "failureBoundary"]) {
		if (input[field] !== undefined && !isNonEmptyString(input[field])) {
			errors.push(`${field} must be a non-empty string`);
		}
	}

	if (
		input.evidence !== undefined &&
		!isNonEmptyString(input.evidence) &&
		!isRecord(input.evidence)
	) {
		errors.push("evidence must be a non-empty string or object");
	}

	for (const field of ["nonGoals", "invariants", "assumptions"]) {
		if (input[field] !== undefined && !isStringOrStringArray(input[field])) {
			errors.push(`${field} must be a string or string array`);
		}
	}

	return { ok: missing.length === 0 && errors.length === 0, missing, errors };
}

/**
 * @typedef {object} RunControlPolicy
 * @property {number} confidence
 * @property {number|"low"|"medium"|"high"|"critical"} risk
 * @property {number} reversibility
 * @property {"unit"|"slice"|"milestone"|"project"|"org"} blastRadius
 * @property {number} cost
 * @property {boolean} legal
 * @property {"none"|"internal"|"customer-visible"|"customer-critical"} customerImpact
 */

/**
 * Validate run-control policy dimensions before autonomous action scoring.
 *
 * Purpose: keep run-control decisions grounded in bounded, typed policy inputs
 * instead of implicit judgment calls.
 *
 * Consumer: SF gates that decide whether work can proceed autonomously,
 * assisted, or with manual attention.
 *
 * @param {unknown} input
 * @returns {{ ok: boolean, errors: string[] }}
 */
export function validateRunControlPolicy(input) {
	const errors = [];

	if (!isRecord(input)) {
		return { ok: false, errors: ["input must be an object"] };
	}

	if (!isUnitInterval(input.confidence)) {
		errors.push("confidence must be a number between 0 and 1");
	}
	if (
		!(
			isUnitInterval(input.risk) ||
			RISK_LEVELS.includes(/** @type {string} */ (input.risk))
		)
	) {
		errors.push(
			"risk must be a number between 0 and 1 or low|medium|high|critical",
		);
	}
	if (!isUnitInterval(input.reversibility)) {
		errors.push("reversibility must be a number between 0 and 1");
	}
	if (
		!BLAST_RADIUS_LEVELS.includes(/** @type {string} */ (input.blastRadius))
	) {
		errors.push("blastRadius must be unit|slice|milestone|project|org");
	}
	if (!isFiniteNumber(input.cost) || input.cost < 0) {
		errors.push("cost must be a non-negative number");
	}
	if (typeof input.legal !== "boolean") {
		errors.push("legal must be a boolean");
	}
	if (
		!CUSTOMER_IMPACT_LEVELS.includes(
			/** @type {string} */ (input.customerImpact),
		)
	) {
		errors.push(
			"customerImpact must be none|internal|customer-visible|customer-critical",
		);
	}

	return { ok: errors.length === 0, errors };
}

/**
 * Score a run-control policy as a deterministic 0..1 actionability value.
 *
 * Purpose: give autonomous run-control gates a stable ordering where higher
 * confidence and reversibility, lower risk, narrower blast radius, lower cost,
 * no legal scope, and lower customer impact all increase actionability.
 *
 * Consumer: SF policy gates that need a scalar threshold without losing the
 * underlying typed policy dimensions.
 *
 * @param {RunControlPolicy} policy
 * @returns {number}
 */
export function scoreRunControlPolicy(policy) {
	if (!isRecord(policy)) {
		return 0;
	}

	const blastRadius =
		BLAST_RADIUS_SCORES[
			/** @type {keyof typeof BLAST_RADIUS_SCORES} */ (policy.blastRadius)
		] ?? 0;
	const customerImpact =
		CUSTOMER_IMPACT_SCORES[
			/** @type {keyof typeof CUSTOMER_IMPACT_SCORES} */ (policy.customerImpact)
		] ?? 0;

	return clamp01(
		clamp01(policy.confidence) * 0.2 +
			normalizeRisk(policy.risk) * 0.2 +
			clamp01(policy.reversibility) * 0.15 +
			blastRadius * 0.15 +
			normalizeCost(policy.cost) * 0.1 +
			(policy.legal === false ? 1 : 0) * 0.1 +
			customerImpact * 0.1,
	);
}

/**
 * @typedef {object} GateResult
 * @property {"pass"|"fail"|"retry"|"manual-attention"} outcome
 * @property {"policy"|"verification"|"execution"|"artifact"|"git"|"timeout"|"input"|"closeout"|"manual-attention"|"unknown"} failureClass
 * @property {string} rationale
 * @property {string} [findings]
 * @property {string} [recommendation]
 */

/**
 * Build a validated gate result object.
 *
 * Purpose: keep gate closeout payloads shaped consistently across policy,
 * verification, execution, artifact, git, timeout, input, and manual paths.
 *
 * Consumer: SF gate runners and closeout recorders that exchange plain
 * serializable gate results.
 *
 * @param {"pass"|"fail"|"retry"|"manual-attention"} outcome
 * @param {{ failureClass?: "policy"|"verification"|"execution"|"artifact"|"git"|"timeout"|"input"|"closeout"|"manual-attention"|"unknown", rationale?: string, findings?: string, recommendation?: string }} opts
 * @returns {GateResult}
 */
export function buildGateResult(outcome, opts = {}) {
	if (!GATE_OUTCOMES.includes(outcome)) {
		throw new TypeError(`invalid gate outcome: ${String(outcome)}`);
	}

	const failureClass = opts.failureClass ?? "unknown";
	if (!FAILURE_CLASSES.includes(failureClass)) {
		throw new TypeError(`invalid failureClass: ${String(failureClass)}`);
	}
	if (!isNonEmptyString(opts.rationale)) {
		throw new TypeError("rationale must be a non-empty string");
	}

	const result = { outcome, failureClass, rationale: opts.rationale };
	if (opts.findings !== undefined) {
		result.findings = opts.findings;
	}
	if (opts.recommendation !== undefined) {
		result.recommendation = opts.recommendation;
	}
	return result;
}

/**
 * Return true when a value has the minimum valid GateResult contract.
 *
 * Purpose: let gate consumers fail closed on malformed closeout payloads before
 * recording or acting on them.
 *
 * Consumer: SF gate result readers that accept plain JSON from multiple gate
 * implementations.
 *
 * @param {unknown} value
 * @returns {value is GateResult}
 */
export function isGateResult(value) {
	return (
		isRecord(value) &&
		GATE_OUTCOMES.includes(/** @type {string} */ (value.outcome)) &&
		isNonEmptyString(value.rationale) &&
		(value.failureClass === undefined ||
			FAILURE_CLASSES.includes(/** @type {string} */ (value.failureClass)))
	);
}

function isRecord(value) {
	return value !== null && typeof value === "object" && !Array.isArray(value);
}

function isNonEmptyString(value) {
	return typeof value === "string" && value.trim().length > 0;
}

function isStringOrStringArray(value) {
	return (
		isNonEmptyString(value) ||
		(Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString))
	);
}

function hasNonEmptyValue(value) {
	return (
		isNonEmptyString(value) || isStringOrStringArray(value) || isRecord(value)
	);
}

function isFiniteNumber(value) {
	return typeof value === "number" && Number.isFinite(value);
}

function isUnitInterval(value) {
	return isFiniteNumber(value) && value >= 0 && value <= 1;
}

function clamp01(value) {
	if (!isFiniteNumber(value)) {
		return 0;
	}
	return Math.min(1, Math.max(0, value));
}

function normalizeRisk(value) {
	if (isUnitInterval(value)) {
		return 1 - value;
	}
	return RISK_SCORES[/** @type {keyof typeof RISK_SCORES} */ (value)] ?? 0;
}

function normalizeCost(value) {
	if (!isFiniteNumber(value) || value < 0) {
		return 0;
	}
	return 1 / (1 + value / 100);
}
