/**
 * requirement-status-sweeper.js — auto-transition R-entries based on DB state.
 *
 * Purpose: walk requirements rows with status='active' and transition each
 * to 'validated' / 'resolved' / 'superseded' when the supporting evidence
 * indicates the work is done.
 *
 * Three rules:
 *  1. Operational R with `Source IDs: sf-...` in description → if ALL source
 *     self_feedback rows have resolved_at != NULL → set status='resolved'
 *     and resolved_reason='evidence-backlog-drained-by-sweeper-<date>'.
 *  2. Capability/architecture/functional R with non-empty supporting_slices
 *     JSON array → if ALL referenced slice rows have status='complete' → set
 *     status='validated'.
 *  3. R with supersedes_by pointing to a row where that row's status ∈
 *     {'resolved','validated','superseded','cancelled'} → set this R's
 *     status='superseded'.
 *
 * Logs each transition as self-feedback (kind: 'r-entry-auto-transition',
 * severity: 'low') with {fromStatus, toStatus, rule, reason}.
 *
 * Consumer: UokGate (type=verification, id=requirement-status-sweeper). NOT
 * registered here — registration is a follow-up slice once Layer A lands
 * its parallel edit of gate-registry-bootstrap.js.
 */

import { _getAdapter } from "./sf-db-core.js";
import { insertSelfFeedbackEntry } from "./sf-db-self-feedback.js";

// ─── Constants ────────────────────────────────────────────────────────────────

const ALL_RULES = [
	"operational-evidence-drained",
	"supporting-slices-complete",
	"supersedes-resolved",
];

const SUPERSEDED_TERMINAL_STATUSES = new Set([
	"resolved",
	"validated",
	"superseded",
	"cancelled",
]);

// ─── Helpers ──────────────────────────────────────────────────────────────────

/**
 * Parse `Source IDs: sf-xxx, sf-yyy` from a description string.
 * Returns an array of IDs, or [] if the marker is absent.
 */
function parseSourceIds(description) {
	if (typeof description !== "string") return [];
	const marker = "Source IDs:";
	const idx = description.indexOf(marker);
	if (idx === -1) return [];
	// Take everything after the marker up to a blank line or end of string.
	const afterMarker = description.slice(idx + marker.length);
	const paragraphEnd = afterMarker.search(/\n\s*\n/);
	const raw =
		paragraphEnd === -1 ? afterMarker : afterMarker.slice(0, paragraphEnd);
	return raw
		.split(",")
		.map((s) => s.trim())
		.filter(Boolean);
}

/**
 * Parse supporting_slices JSON field → array of "milestoneId/sliceId" strings.
 * Returns [] for any parse failure or empty array.
 */
function parseSupportingSlices(raw) {
	if (typeof raw !== "string" || raw.trim() === "" || raw.trim() === "[]")
		return [];
	try {
		const parsed = JSON.parse(raw);
		if (!Array.isArray(parsed)) return [];
		return parsed.filter((s) => typeof s === "string" && s.length > 0);
	} catch {
		return [];
	}
}

/**
 * Record a self-feedback entry for an auto-transition (non-fatal).
 */
function logTransition(req, toStatus, rule, reason) {
	try {
		const ts = new Date().toISOString();
		const id = `sf-sweeper-${req.id}-${rule}-${Date.now()}`;
		insertSelfFeedbackEntry({
			id,
			ts,
			kind: "r-entry-auto-transition",
			severity: "low",
			blocking: false,
			repoIdentity: "forge",
			sfVersion: "",
			basePath: "",
			summary: `R-entry ${req.id} auto-transitioned ${req.status} → ${toStatus} via rule ${rule}`,
			evidence: JSON.stringify({
				fromStatus: req.status,
				toStatus,
				rule,
				reason,
			}),
			suggestedFix: "",
			full_json: JSON.stringify({
				id,
				ts,
				kind: "r-entry-auto-transition",
				severity: "low",
				blocking: false,
				repoIdentity: "forge",
				sfVersion: "",
				basePath: "",
				summary: `R-entry ${req.id} auto-transitioned ${req.status} → ${toStatus} via rule ${rule}`,
				evidence: JSON.stringify({
					fromStatus: req.status,
					toStatus,
					rule,
					reason,
				}),
				suggestedFix: "",
				fromStatus: req.status,
				toStatus,
				rule,
				reason,
			}),
		});
	} catch {
		// non-fatal
	}
}

/**
 * Record a dangling-chain warning as self-feedback (non-fatal).
 */
function logDanglingChain(reqId, supersededById) {
	try {
		const ts = new Date().toISOString();
		const id = `sf-sweeper-dangling-${reqId}-${Date.now()}`;
		insertSelfFeedbackEntry({
			id,
			ts,
			kind: "r-entry-dangling-supersedes",
			severity: "low",
			blocking: false,
			repoIdentity: "forge",
			sfVersion: "",
			basePath: "",
			summary: `R-entry ${reqId} references supersedes_by=${supersededById} which does not exist`,
			evidence: JSON.stringify({ reqId, supersededById }),
			suggestedFix: "Review and correct the supersedes_by reference",
			full_json: JSON.stringify({
				id,
				ts,
				kind: "r-entry-dangling-supersedes",
				severity: "low",
				blocking: false,
				repoIdentity: "forge",
				sfVersion: "",
				basePath: "",
				summary: `R-entry ${reqId} references supersedes_by=${supersededById} which does not exist`,
				evidence: JSON.stringify({ reqId, supersededById }),
				suggestedFix: "Review and correct the supersedes_by reference",
			}),
		});
	} catch {
		// non-fatal
	}
}

// ─── Rule implementations ─────────────────────────────────────────────────────

/**
 * Rule 1: operational-evidence-drained
 * Operational R entries carry "Source IDs: sf-..." in their description.
 * If all listed self_feedback rows are resolved, the R transitions to 'resolved'.
 */
function applyOperationalEvidenceDrained(db, req) {
	if (!req.description) return null;
	const sourceIds = parseSourceIds(req.description);
	if (sourceIds.length === 0) return null;

	// Check each source ID — all must be resolved (resolved_at NOT NULL)
	for (const sfId of sourceIds) {
		const row = db
			.prepare("SELECT resolved_at FROM self_feedback WHERE id = ?")
			.get(sfId);
		// If not found or not resolved, rule doesn't fire
		if (!row || !row["resolved_at"]) return null;
	}

	const date = new Date().toISOString().slice(0, 10);
	return {
		toStatus: "resolved",
		rule: "operational-evidence-drained",
		reason: `evidence-backlog-drained-by-sweeper-${date}`,
		updates: {
			status: "resolved",
			notes: `evidence-backlog-drained-by-sweeper-${date}`,
		},
	};
}

/**
 * Rule 2: supporting-slices-complete
 * Capability/architecture/functional R entries with non-empty supporting_slices.
 * If all referenced slices are complete, the R transitions to 'validated'.
 */
function applySupportingSlicesComplete(db, req) {
	const sliceRefs = parseSupportingSlices(req.supporting_slices);
	if (sliceRefs.length === 0) return null;

	for (const ref of sliceRefs) {
		// Expected format: "milestoneId/sliceId"
		const slashIdx = ref.indexOf("/");
		if (slashIdx === -1) return null; // malformed ref, skip rule

		const milestoneId = ref.slice(0, slashIdx);
		const sliceId = ref.slice(slashIdx + 1);

		const row = db
			.prepare("SELECT status FROM slices WHERE milestone_id = ? AND id = ?")
			.get(milestoneId, sliceId);

		if (!row || row["status"] !== "complete") return null;
	}

	return {
		toStatus: "validated",
		rule: "supporting-slices-complete",
		reason: `all ${sliceRefs.length} supporting slice(s) complete`,
		updates: { status: "validated" },
	};
}

/**
 * Rule 3: supersedes-resolved
 * If this R has a supersedes_by pointing to a terminal R, mark this one superseded.
 * Logs a dangling-chain warning if the target doesn't exist.
 */
function applySupersedes(db, req) {
	const supersededBy = req.superseded_by;
	if (!supersededBy) return null;

	const targetRow = db
		.prepare("SELECT id, status FROM requirements WHERE id = ?")
		.get(supersededBy);

	if (!targetRow) {
		// Dangling chain — log as self-feedback but don't transition
		logDanglingChain(req.id, supersededBy);
		return null;
	}

	if (!SUPERSEDED_TERMINAL_STATUSES.has(targetRow["status"])) return null;

	return {
		toStatus: "superseded",
		rule: "supersedes-resolved",
		reason: `superseded by ${supersededBy} (status=${targetRow["status"]})`,
		updates: { status: "superseded" },
	};
}

// ─── Main export ──────────────────────────────────────────────────────────────

/**
 * Walk all active requirements and apply transition rules.
 *
 * @param {string} _basePath - base path of the project (unused directly;
 *   DB adapter is process-global, kept for API symmetry with other sweepers).
 * @param {object} [options]
 * @param {boolean} [options.dryRun=false] - if true, returns transitions but
 *   does not write to DB.
 * @param {string[]} [options.includeRules] - subset of rules to apply; defaults
 *   to all three.
 * @returns {{ transitioned: Array<{id,fromStatus,toStatus,rule}>, totalChecked: number, durationMs: number }}
 */
export async function sweepRequirementStatuses(_basePath, options = {}) {
	const startMs = Date.now();
	const dryRun = options.dryRun === true;
	const includeRules = Array.isArray(options.includeRules)
		? options.includeRules
		: ALL_RULES;

	const db = _getAdapter();
	if (!db) {
		return { transitioned: [], totalChecked: 0, durationMs: 0 };
	}

	// Fetch all active requirements (status='active', superseded_by IS NULL via
	// the active_requirements view).
	const rows = db
		.prepare("SELECT * FROM requirements WHERE status = 'active'")
		.all();

	const transitioned = [];

	for (const rawRow of rows) {
		const req = {
			id: rawRow["id"],
			class: rawRow["class"],
			status: rawRow["status"],
			description: rawRow["description"],
			why: rawRow["why"],
			source: rawRow["source"],
			primary_owner: rawRow["primary_owner"],
			supporting_slices: rawRow["supporting_slices"],
			validation: rawRow["validation"],
			notes: rawRow["notes"],
			full_content: rawRow["full_content"],
			superseded_by: rawRow["superseded_by"] ?? null,
		};

		let transition = null;

		// Apply rules in priority order; first match wins.
		if (!transition && includeRules.includes("operational-evidence-drained")) {
			transition = applyOperationalEvidenceDrained(db, req);
		}
		if (!transition && includeRules.includes("supporting-slices-complete")) {
			transition = applySupportingSlicesComplete(db, req);
		}
		if (!transition && includeRules.includes("supersedes-resolved")) {
			transition = applySupersedes(db, req);
		}

		if (!transition) continue;

		const entry = {
			id: req.id,
			fromStatus: req.status,
			toStatus: transition.toStatus,
			rule: transition.rule,
		};
		transitioned.push(entry);

		if (!dryRun) {
			// Apply the status update; preserve all other columns.
			db.prepare(`UPDATE requirements SET status = :status WHERE id = :id`).run(
				{ ":status": transition.toStatus, ":id": req.id },
			);

			// For rule 1: write reason into notes field as well.
			if (transition.rule === "operational-evidence-drained") {
				const existingNotes = req.notes ?? "";
				const sep = existingNotes.trim().length > 0 ? "\n" : "";
				db.prepare(`UPDATE requirements SET notes = :notes WHERE id = :id`).run(
					{
						":notes": `${existingNotes}${sep}${transition.reason}`,
						":id": req.id,
					},
				);
			}

			logTransition(
				req,
				transition.toStatus,
				transition.rule,
				transition.reason,
			);
		}
	}

	return {
		transitioned,
		totalChecked: rows.length,
		durationMs: Date.now() - startMs,
	};
}

// ─── UokGate wrapper ──────────────────────────────────────────────────────────

/**
 * UokGate descriptor for the requirement status sweeper.
 * Registration is deferred — see module-level comment.
 */
export const requirementStatusSweeperGate = {
	id: "requirement-status-sweeper",
	type: "verification",
	async execute(ctx) {
		const result = await sweepRequirementStatuses(ctx.basePath, ctx.options);
		return result.transitioned.length === 0
			? {
					outcome: "pass",
					failureClass: null,
					rationale: `requirement sweep: 0 of ${result.totalChecked} needed transition`,
				}
			: {
					outcome: "manual-attention",
					failureClass: "verification",
					rationale: `requirement sweep: ${result.transitioned.length} of ${result.totalChecked} transitioned`,
					findings: result.transitioned,
				};
	},
};
