import { SF_STALE_STATE, SFError } from "../errors.js";
import { _getAdapter, rowToSelfFeedback } from "./sf-db-core.js";

/**
 * Severity-derived default impact_score (sf-mp4rxkx0-fkt3e2). Operators can
 * override per-entry via the explicit impact_score field; this default is
 * only consulted when no explicit score is supplied. Tuned so high/critical
 * entries naturally outrank medium ones, but the gap leaves room for an
 * operator to bump a "low" entry above a routine "medium" by setting score
 * 60 explicitly.
 */
function defaultImpactForSeverity(severity) {
	switch (severity) {
		case "critical":
			return 95;
		case "high":
			return 80;
		case "medium":
			return 50;
		case "low":
			return 20;
		default:
			return 50;
	}
}

export function insertSelfFeedbackEntry(entry) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	const occurred = entry.occurredIn ?? {};
	const impactScore =
		typeof entry.impactScore === "number"
			? entry.impactScore
			: defaultImpactForSeverity(entry.severity);
	const effortEstimate =
		typeof entry.effortEstimate === "number" ? entry.effortEstimate : null;
	// purpose_anchor (sf-db v71, ADR-0000 restoration): free-text fragment of
	// the milestone vision or slice goal sentence this entry is filed against.
	// NULL is allowed for legacy rows / callers that don't yet supply it.
	const purposeAnchor =
		typeof entry.purposeAnchor === "string" && entry.purposeAnchor.trim() !== ""
			? entry.purposeAnchor.trim()
			: null;
	currentDb
		.prepare(`INSERT INTO self_feedback (
      id, ts, kind, severity, blocking, repo_identity, sf_version, base_path,
      unit_type, milestone_id, slice_id, task_id, summary, evidence, suggested_fix, full_json,
      resolved_at, resolved_reason, resolved_by_sf_version, resolved_evidence_json, resolved_criteria_json,
      impact_score, effort_estimate, purpose_anchor
    ) VALUES (
      :id, :ts, :kind, :severity, :blocking, :repo_identity, :sf_version, :base_path,
      :unit_type, :milestone_id, :slice_id, :task_id, :summary, :evidence, :suggested_fix, :full_json,
      :resolved_at, :resolved_reason, :resolved_by_sf_version, :resolved_evidence_json, :resolved_criteria_json,
      :impact_score, :effort_estimate, :purpose_anchor
    )
    ON CONFLICT(id) DO NOTHING`)
		.run({
			":id": entry.id,
			":ts": entry.ts,
			":kind": entry.kind,
			":severity": entry.severity,
			":blocking": entry.blocking ? 1 : 0,
			":repo_identity": entry.repoIdentity ?? "",
			":sf_version": entry.sfVersion ?? "",
			":base_path": entry.basePath ?? "",
			":unit_type": occurred.unitType ?? null,
			":milestone_id": occurred.milestone ?? null,
			":slice_id": occurred.slice ?? null,
			":task_id": occurred.task ?? null,
			":summary": entry.summary ?? "",
			":evidence": entry.evidence ?? "",
			":suggested_fix": entry.suggestedFix ?? "",
			":full_json": JSON.stringify(entry),
			":resolved_at": entry.resolvedAt ?? null,
			":resolved_reason": entry.resolvedReason ?? null,
			":resolved_by_sf_version": entry.resolvedBySfVersion ?? null,
			":resolved_evidence_json": entry.resolvedEvidence
				? JSON.stringify(entry.resolvedEvidence)
				: null,
			":resolved_criteria_json": entry.resolvedCriteriaMet
				? JSON.stringify(entry.resolvedCriteriaMet)
				: null,
			":impact_score": impactScore,
			":effort_estimate": effortEstimate,
			":purpose_anchor": purposeAnchor,
		});
}

export function listSelfFeedbackEntries(options) {
	const currentDb = _getAdapter();
	if (!currentDb) return [];
	// Optional filter: substring match against purpose_anchor so triage can
	// scope to "rows filed against milestone X's vision" without pulling the
	// full table client-side (ADR-0000, v71). LIKE with % wildcards keeps the
	// match permissive — operators paste a sentence fragment, not the full
	// canonical anchor text.
	const purposeFragment =
		options &&
		typeof options.purpose === "string" &&
		options.purpose.trim() !== ""
			? options.purpose.trim()
			: null;
	if (purposeFragment) {
		const rows = currentDb
			.prepare(
				"SELECT * FROM self_feedback WHERE purpose_anchor LIKE :pattern ORDER BY ts ASC, id ASC",
			)
			.all({ ":pattern": `%${purposeFragment}%` });
		return rows.map(rowToSelfFeedback);
	}
	const rows = currentDb
		.prepare("SELECT * FROM self_feedback ORDER BY ts ASC, id ASC")
		.all();
	return rows.map(rowToSelfFeedback);
}

/**
 * Allowed relation kinds between self-feedback entries (sf-mp4rxkwx-jz0soh,
 * v64 schema migration). The CHECK constraint on the table enforces this
 * at the DB layer; this constant exists so callers can validate before
 * attempting an insert and surface a structured error.
 */
export const SELF_FEEDBACK_RELATION_KINDS = Object.freeze([
	"supersedes",
	"duplicate_of",
	"blocks",
	"root_cause_of",
	"partial_fix_of",
]);

/**
 * Insert a directed relation between two self-feedback entries.
 *
 * `fromId` and `toId` must reference existing self_feedback rows (FK).
 * Self-loops are rejected by a CHECK constraint. The same (from, to, kind)
 * triple is unique (composite primary key) but multiple kinds between the
 * same pair are allowed (e.g. supersedes AND blocks).
 *
 * Returns true when a row was inserted, false when the triple already
 * existed. Throws SFError on FK / CHECK violations or DB unavailability so
 * callers see structured failures rather than silent drops.
 */
export function linkEntries(fromId, toId, relationKind) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	if (!SELF_FEEDBACK_RELATION_KINDS.includes(relationKind)) {
		throw new SFError(
			SF_STALE_STATE,
			`linkEntries: invalid relation_kind "${relationKind}"; allowed: ${SELF_FEEDBACK_RELATION_KINDS.join(", ")}`,
		);
	}
	// Plain INSERT (NOT "INSERT OR IGNORE") so CHECK and FK violations
	// surface as thrown errors. INSERT OR IGNORE silently swallows ALL
	// constraint failures — including the from_id != to_id self-loop guard
	// and the FK to self_feedback.id — which would let invalid links land
	// quietly. We only want idempotency for the composite-PK duplicate
	// case, which we detect by error message and return false for.
	try {
		const result = currentDb
			.prepare(
				`INSERT INTO self_feedback_relations
				   (from_id, to_id, relation_kind, created_at)
				 VALUES (:from_id, :to_id, :relation_kind, :created_at)`,
			)
			.run({
				":from_id": fromId,
				":to_id": toId,
				":relation_kind": relationKind,
				":created_at": new Date().toISOString(),
			});
		return result.changes > 0;
	} catch (err) {
		const msg =
			err && typeof err === "object" && "message" in err
				? String(err.message)
				: "";
		if (
			msg.includes("UNIQUE constraint failed") ||
			msg.includes("PRIMARY KEY constraint failed")
		) {
			return false; // idempotent: same triple already linked
		}
		throw err;
	}
}

/**
 * Return all relations involving the given entry id, in both directions.
 *
 * Each result is { id, relationKind, direction } where:
 *   direction === "outbound" — `entryId` is the from-side; `id` is the to-side
 *   direction === "inbound"  — `entryId` is the to-side;   `id` is the from-side
 *
 * The two-direction surface lets callers ask "what does this entry
 * reference, and what references it" in one call. Order is unspecified.
 */
export function getRelatedEntries(entryId) {
	const currentDb = _getAdapter();
	if (!currentDb) return [];
	const out = [];
	const outbound = currentDb
		.prepare(
			"SELECT to_id AS id, relation_kind FROM self_feedback_relations WHERE from_id = :id",
		)
		.all({ ":id": entryId });
	for (const row of outbound) {
		out.push({
			id: row.id,
			relationKind: row.relation_kind,
			direction: "outbound",
		});
	}
	const inbound = currentDb
		.prepare(
			"SELECT from_id AS id, relation_kind FROM self_feedback_relations WHERE to_id = :id",
		)
		.all({ ":id": entryId });
	for (const row of inbound) {
		out.push({
			id: row.id,
			relationKind: row.relation_kind,
			direction: "inbound",
		});
	}
	return out;
}

export function resolveSelfFeedbackEntry(entryId, resolution) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	const existing = currentDb
		.prepare("SELECT * FROM self_feedback WHERE id = :id")
		.get({ ":id": entryId });
	if (!existing) return false;
	if (existing["resolved_at"] && hasCredibleResolutionEvidence(existing)) {
		return false;
	}
	const resolvedAt = resolution.resolvedAt ?? new Date().toISOString();
	const entry = {
		...rowToSelfFeedback(existing),
		resolvedAt,
		resolvedReason: resolution.reason,
		resolvedBySfVersion: resolution.resolvedBySfVersion ?? "",
		resolvedEvidence: resolution.evidence,
	};
	if (resolution.criteriaMet)
		entry.resolvedCriteriaMet = resolution.criteriaMet;
	const result = currentDb
		.prepare(`UPDATE self_feedback SET
      full_json = :full_json,
      resolved_at = :resolved_at,
      resolved_reason = :resolved_reason,
      resolved_by_sf_version = :resolved_by_sf_version,
      resolved_evidence_json = :resolved_evidence_json,
      resolved_criteria_json = :resolved_criteria_json
    WHERE id = :id`)
		.run({
			":id": entryId,
			":full_json": JSON.stringify(entry),
			":resolved_at": resolvedAt,
			":resolved_reason": resolution.reason ?? "",
			":resolved_by_sf_version": resolution.resolvedBySfVersion ?? "",
			":resolved_evidence_json": resolution.evidence
				? JSON.stringify(resolution.evidence)
				: null,
			":resolved_criteria_json": resolution.criteriaMet
				? JSON.stringify(resolution.criteriaMet)
				: null,
		});
	return result.changes > 0;
}

function hasCredibleResolutionEvidence(row) {
	try {
		const parsed = row["resolved_evidence_json"]
			? JSON.parse(row["resolved_evidence_json"])
			: null;
		return (
			parsed?.kind === "agent-fix" ||
			parsed?.kind === "human-clear" ||
			parsed?.kind === "promoted-to-requirement"
		);
	} catch {
		return false;
	}
}
