import { _getAdapter, hasTaskSpecIntent, insertTaskSpecIfAbsent, rowToTask, safeParseJsonArray, transaction } from './sf-db-core.js';
import { SF_STALE_STATE, SFError } from '../errors.js';
import { normalizeSchedulerStatus, normalizeTaskStatus, taskFrontmatterFromRecord, withTaskFrontmatter } from '../task-frontmatter.js';
import { logWarning } from '../workflow-logger.js';

export function insertTask(t) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(`INSERT INTO tasks (
      milestone_id, slice_id, id, title, status, task_status, one_liner, narrative,
      verification_result, verification_status, duration, completed_at, blocker_discovered,
      deviations, known_issues, key_files, key_decisions, full_summary_md,
      description, estimate, files, verify, inputs, expected_output, observability_impact, sequence
    ) VALUES (
      :milestone_id, :slice_id, :id, :title, :status, :task_status, :one_liner, :narrative,
      :verification_result, :verification_status, :duration, :completed_at, :blocker_discovered,
      :deviations, :known_issues, :key_files, :key_decisions, :full_summary_md,
      :description, :estimate, :files, :verify, :inputs, :expected_output, :observability_impact, :sequence
    )
    ON CONFLICT(milestone_id, slice_id, id) DO UPDATE SET
      title = CASE WHEN NULLIF(:title, '') IS NOT NULL THEN :title ELSE tasks.title END,
      status = :status,
      task_status = :task_status,
      one_liner = :one_liner,
      narrative = :narrative,
      verification_result = :verification_result,
      verification_status = :verification_status,
      duration = :duration,
      completed_at = :completed_at,
      blocker_discovered = :blocker_discovered,
      deviations = :deviations,
      known_issues = :known_issues,
      key_files = :key_files,
      key_decisions = :key_decisions,
      full_summary_md = :full_summary_md,
      description = CASE WHEN NULLIF(:description, '') IS NOT NULL THEN :description ELSE tasks.description END,
      estimate = CASE WHEN NULLIF(:estimate, '') IS NOT NULL THEN :estimate ELSE tasks.estimate END,
      files = CASE WHEN NULLIF(:files, '[]') IS NOT NULL THEN :files ELSE tasks.files END,
      verify = CASE WHEN NULLIF(:verify, '') IS NOT NULL THEN :verify ELSE tasks.verify END,
      inputs = CASE WHEN NULLIF(:inputs, '[]') IS NOT NULL THEN :inputs ELSE tasks.inputs END,
      expected_output = CASE WHEN NULLIF(:expected_output, '[]') IS NOT NULL THEN :expected_output ELSE tasks.expected_output END,
      observability_impact = CASE WHEN NULLIF(:observability_impact, '') IS NOT NULL THEN :observability_impact ELSE tasks.observability_impact END,
      sequence = :sequence`)
		.run({
			":milestone_id": t.milestoneId,
			":slice_id": t.sliceId,
			":id": t.id,
			":title": t.title ?? "",
			":status": t.status ?? "pending",
			":task_status": normalizeTaskStatus(t.taskStatus ?? t.status) ?? "todo",
			":one_liner": t.oneLiner ?? "",
			":narrative": t.narrative ?? "",
			":verification_result": t.verificationResult ?? "",
			":verification_status": t.verificationStatus ?? "",
			":duration": t.duration ?? "",
			":completed_at":
				t.status === "done" || t.status === "complete"
					? new Date().toISOString()
					: null,
			":blocker_discovered": t.blockerDiscovered ? 1 : 0,
			":deviations": t.deviations ?? "",
			":known_issues": t.knownIssues ?? "",
			":key_files": JSON.stringify(t.keyFiles ?? []),
			":key_decisions": JSON.stringify(t.keyDecisions ?? []),
			":full_summary_md": t.fullSummaryMd ?? "",
			":description": t.planning?.description ?? "",
			":estimate": t.planning?.estimate ?? "",
			":files": JSON.stringify(t.planning?.files ?? []),
			":verify": t.planning?.verify ?? "",
			":inputs": JSON.stringify(t.planning?.inputs ?? []),
			":expected_output": JSON.stringify(t.planning?.expectedOutput ?? []),
			":observability_impact": t.planning?.observabilityImpact ?? "",
			":sequence": t.sequence ?? 0,
		});
	if (hasTaskSpecIntent(t.planning)) {
		insertTaskSpecIfAbsent(t.milestoneId, t.sliceId, t.id, t.planning ?? {});
	}
	upsertTaskSchedulerStatus(t.milestoneId, t.sliceId, t.id, "queued", {
		onlyIfAbsent: true,
	});
}

export function insertOrIgnoreTask(args) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(`INSERT OR IGNORE INTO tasks (milestone_id, slice_id, id, title, status, created_at)
     VALUES (:mid, :sid, :tid, :title, 'pending', :ts)`)
		.run({
			":mid": args.milestoneId,
			":sid": args.sliceId,
			":tid": args.taskId,
			":title": args.title,
			":ts": args.createdAt,
		});
}

export function upsertTaskSchedulerStatus(
	milestoneId,
	sliceId,
	taskId,
	status = "queued",
	{ onlyIfAbsent = false } = {},
) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	const schedulerStatus = normalizeSchedulerStatus(status) ?? "queued";
	const sql = onlyIfAbsent
		? `INSERT OR IGNORE INTO task_scheduler (
        milestone_id, slice_id, task_id, status, updated_at
      ) VALUES (
        :milestone_id, :slice_id, :task_id, :status, :updated_at
      )`
		: `INSERT INTO task_scheduler (
        milestone_id, slice_id, task_id, status, updated_at
      ) VALUES (
        :milestone_id, :slice_id, :task_id, :status, :updated_at
      )
      ON CONFLICT(milestone_id, slice_id, task_id) DO UPDATE SET
        status = excluded.status,
        updated_at = excluded.updated_at`;
	currentDb.prepare(sql).run({
		":milestone_id": milestoneId,
		":slice_id": sliceId,
		":task_id": taskId,
		":status": schedulerStatus,
		":updated_at": new Date().toISOString(),
	});
}

export function updateTaskStatus(
	milestoneId,
	sliceId,
	taskId,
	status,
	completedAt,
) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	const taskStatus = normalizeTaskStatus(status) ?? "todo";
	currentDb
		.prepare(`UPDATE tasks SET
      status = :status,
      completed_at = :completed_at,
      task_status = :task_status
     WHERE milestone_id = :milestone_id AND slice_id = :slice_id AND id = :id`)
		.run({
			":status": status,
			":completed_at": completedAt ?? null,
			":task_status": taskStatus,
			":milestone_id": milestoneId,
			":slice_id": sliceId,
			":id": taskId,
		});
}

export function setTaskEscalationPending(
	milestoneId,
	sliceId,
	taskId,
	artifactPath,
) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(`UPDATE tasks
         SET escalation_pending = 1,
             escalation_awaiting_review = 0,
             escalation_artifact_path = :path
       WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`)
		.run({
			":path": artifactPath,
			":mid": milestoneId,
			":sid": sliceId,
			":tid": taskId,
		});
}

export function setTaskEscalationAwaitingReview(
	milestoneId,
	sliceId,
	taskId,
	artifactPath,
) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(`UPDATE tasks
         SET escalation_awaiting_review = 1,
             escalation_pending = 0,
             escalation_artifact_path = :path
       WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`)
		.run({
			":path": artifactPath,
			":mid": milestoneId,
			":sid": sliceId,
			":tid": taskId,
		});
}

export function clearTaskEscalationFlags(milestoneId, sliceId, taskId) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(`UPDATE tasks
         SET escalation_pending = 0,
             escalation_awaiting_review = 0
       WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`)
		.run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
}

export function findUnappliedEscalationOverride(milestoneId, sliceId) {
	const currentDb = _getAdapter();
	if (!currentDb) return null;
	const row = currentDb
		.prepare(`SELECT id, escalation_artifact_path
       FROM tasks
       WHERE milestone_id = :mid
         AND slice_id = :sid
         AND escalation_artifact_path IS NOT NULL
         AND escalation_pending = 0
         AND escalation_awaiting_review = 0
         AND escalation_override_applied = 0
       ORDER BY sequence ASC, id ASC
       LIMIT 1`)
		.get({ ":mid": milestoneId, ":sid": sliceId });
	if (!row || !row.escalation_artifact_path) return null;
	return { taskId: row.id, artifactPath: row.escalation_artifact_path };
}

export function claimEscalationOverride(milestoneId, sliceId, taskId) {
	const currentDb = _getAdapter();
	if (!currentDb) return false;
	const result = currentDb
		.prepare(`UPDATE tasks
         SET escalation_override_applied = 1
       WHERE milestone_id = :mid
         AND slice_id = :sid
         AND id = :tid
         AND escalation_override_applied = 0`)
		.run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
	return (result?.changes ?? 0) > 0;
}

export function setTaskBlockerDiscovered(
	milestoneId,
	sliceId,
	taskId,
	discovered,
) {
	const currentDb = _getAdapter();
	if (!currentDb) return;
	currentDb
		.prepare(
			`UPDATE tasks SET blocker_discovered = :discovered WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
		)
		.run({
			":discovered": discovered ? 1 : 0,
			":mid": milestoneId,
			":sid": sliceId,
			":tid": taskId,
		});
}

export function upsertTaskPlanning(milestoneId, sliceId, taskId, planning) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning);
	const { normalized: frontmatter, errors: fmErrors } =
		taskFrontmatterFromRecord(planning);
	if (fmErrors?.length)
		logWarning(
			"sf-db:upsertTaskPlanning",
			`frontmatter validation errors for ${milestoneId}/${sliceId}/${taskId}: ${fmErrors.join(", ")}`,
		);
	const hasTaskStatus =
		planning.taskStatus !== undefined ||
		planning.task_status !== undefined ||
		planning.status !== undefined;
	currentDb
		.prepare(`UPDATE tasks SET
      title = COALESCE(:title, title),
      description = COALESCE(:description, description),
      estimate = COALESCE(:estimate, estimate),
      files = COALESCE(:files, files),
      verify = COALESCE(:verify, verify),
      inputs = COALESCE(:inputs, inputs),
      expected_output = COALESCE(:expected_output, expected_output),
      observability_impact = COALESCE(:observability_impact, observability_impact),
      full_plan_md = COALESCE(:full_plan_md, full_plan_md),
      risk = :risk,
      mutation_scope = :mutation_scope,
      verification_type = :verification_type,
      plan_approval = :plan_approval,
      task_status = CASE WHEN :has_task_status = 1 THEN :task_status ELSE task_status END,
      estimated_effort = :estimated_effort,
      dependencies = :dependencies,
      blocks_parallel = :blocks_parallel,
      requires_user_input = :requires_user_input,
      auto_retry = :auto_retry,
      max_retries = :max_retries,
      frontmatter_version = :frontmatter_version
     WHERE milestone_id = :milestone_id AND slice_id = :slice_id AND id = :id`)
		.run({
			":milestone_id": milestoneId,
			":slice_id": sliceId,
			":id": taskId,
			":title": planning.title ?? null,
			":description": planning.description ?? null,
			":estimate": planning.estimate ?? null,
			":files": planning.files ? JSON.stringify(planning.files) : null,
			":verify": planning.verify ?? null,
			":inputs": planning.inputs ? JSON.stringify(planning.inputs) : null,
			":expected_output": planning.expectedOutput
				? JSON.stringify(planning.expectedOutput)
				: null,
			":observability_impact": planning.observabilityImpact ?? null,
			":full_plan_md": planning.fullPlanMd ?? null,
			":risk": frontmatter.risk,
			":mutation_scope": frontmatter.mutationScope,
			":verification_type": frontmatter.verification,
			":plan_approval": frontmatter.planApproval,
			":task_status": frontmatter.taskStatus,
			":has_task_status": hasTaskStatus ? 1 : 0,
			":estimated_effort": frontmatter.estimatedEffort,
			":dependencies": JSON.stringify(frontmatter.dependencies),
			":blocks_parallel": frontmatter.blocksParallel ? 1 : 0,
			":requires_user_input": frontmatter.requiresUserInput ? 1 : 0,
			":auto_retry": frontmatter.autoRetry ? 1 : 0,
			":max_retries": frontmatter.maxRetries,
			":frontmatter_version": frontmatter.frontmatterVersion,
		});
	if (
		planning.schedulerStatus !== undefined ||
		planning.scheduler_status !== undefined
	) {
		upsertTaskSchedulerStatus(
			milestoneId,
			sliceId,
			taskId,
			frontmatter.schedulerStatus,
		);
		} else {
			upsertTaskSchedulerStatus(milestoneId, sliceId, taskId, "queued", {
				onlyIfAbsent: true,
			});
		}
	}

export function getTask(milestoneId, sliceId, taskId) {
	const currentDb = _getAdapter();
	if (!currentDb) return null;
	const row = currentDb
		.prepare(
			`SELECT t.*, ts.status AS scheduler_status
			   FROM tasks t
			   LEFT JOIN task_scheduler ts
			     ON t.milestone_id = ts.milestone_id
			    AND t.slice_id = ts.slice_id
			    AND t.id = ts.task_id
			  WHERE t.milestone_id = :mid AND t.slice_id = :sid AND t.id = :tid`,
		)
		.get({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
	if (!row) return null;
	return rowToTask(row);
}

export function setTaskSummaryMd(milestoneId, sliceId, taskId, md) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(
			`UPDATE tasks SET full_summary_md = :md WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
		)
		.run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId, ":md": md });
}

/**
 * Apply on-disk SUMMARY.md frontmatter and body to the DB task row.
 *
 * Purpose: operator-invoked remediation when the DB-driven reconcile refuses
 * to silently import disk state (state-db.js:reconcileSliceTasks). Writes
 * status, completed_at, verification_result, key_files (as JSON), blocker
 * flag, and the full markdown body in a single UPDATE so the row matches the
 * on-disk SUMMARY shape.
 *
 * Consumer: state-reconcile.js: reconcileTaskFromSummary.
 */
export function setTaskSummaryFields(
	milestoneId,
	sliceId,
	taskId,
	{
		status,
		completedAt,
		verificationResult,
		blockerDiscovered,
		keyFiles,
		summaryMd,
	},
) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	currentDb
		.prepare(
			`UPDATE tasks SET
        status = :status,
        completed_at = :completed_at,
        verification_result = :verification_result,
        blocker_discovered = :blocker_discovered,
        key_files = :key_files,
        full_summary_md = :summary_md
       WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
		)
		.run({
			":mid": milestoneId,
			":sid": sliceId,
			":tid": taskId,
			":status": status,
			":completed_at": completedAt ?? null,
			":verification_result": verificationResult ?? "",
			":blocker_discovered": blockerDiscovered ? 1 : 0,
			":key_files": JSON.stringify(keyFiles ?? []),
			":summary_md": summaryMd ?? "",
		});
}

export function getActiveTaskFromDb(milestoneId, sliceId) {
	const currentDb = _getAdapter();
	if (!currentDb) return null;
	const row = currentDb
		.prepare(
			"SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY sequence, id LIMIT 1",
		)
		.get({ ":mid": milestoneId, ":sid": sliceId });
	if (!row) return null;
	return rowToTask(row);
}

export function getActiveTaskIdFromDb(milestoneId, sliceId) {
	const currentDb = _getAdapter();
	if (!currentDb) return null;
	const row = currentDb
		.prepare(
			"SELECT id, status, title FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY sequence, id LIMIT 1",
		)
		.get({ ":mid": milestoneId, ":sid": sliceId });
	if (!row) return null;
	return {
		id: row["id"],
		status: row["status"],
		title: row["title"],
	};
}

export function deleteTask(milestoneId, sliceId, taskId) {
	const currentDb = _getAdapter();
	if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
	transaction(() => {
		// Must delete verification_evidence first (FK constraint)
		currentDb
			.prepare(
				`DELETE FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid`,
			)
			.run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
		currentDb
			.prepare(
				`DELETE FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND id = :tid`,
			)
			.run({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
	});
}

export function listEscalationArtifacts(milestoneId, includeResolved = false) {
	const currentDb = _getAdapter();
	if (!currentDb) return [];
	const filter = includeResolved
		? "escalation_artifact_path IS NOT NULL"
		: "(escalation_pending = 1 OR escalation_awaiting_review = 1) AND escalation_artifact_path IS NOT NULL";
	const rows = currentDb
		.prepare(
			`SELECT * FROM tasks WHERE milestone_id = :mid AND ${filter} ORDER BY slice_id, sequence, id`,
		)
		.all({ ":mid": milestoneId });
	return rows.map(rowToTask);
}

export function getSliceTasks(milestoneId, sliceId) {
	const currentDb = _getAdapter();
	if (!currentDb) return [];
	const rows = currentDb
		.prepare(
			`SELECT t.*, ts.status AS scheduler_status
			   FROM tasks t
			   LEFT JOIN task_scheduler ts
			     ON t.milestone_id = ts.milestone_id
			    AND t.slice_id = ts.slice_id
			    AND t.id = ts.task_id
			  WHERE t.milestone_id = :mid AND t.slice_id = :sid
			  ORDER BY t.sequence, t.id`,
		)
		.all({ ":mid": milestoneId, ":sid": sliceId });
	return rows.map(rowToTask);
}
