/**
 * Schedule Store — DB-primary persistence for scheduled entries.
 *
 * Purpose: provide durable, queryable storage for schedule entries with
 * status-grouping semantics (latest entry per ID wins) and time-based queries.
 *
 * Consumer: schedule CLI commands (S02), autonomous dispatch reminders, and UI overlays.
 */
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { sfRoot } from "../paths.js";
import {
	countScheduleEntries,
	getScheduleEntries,
	insertScheduleEntry,
	openDatabase,
} from "../sf-db.js";
import { sfHome } from "../sf-home.js";

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

const FILENAME = "schedule.jsonl";
const SCHEDULE_SCHEMA_VERSION = 1;

// ─── Public API ─────────────────────────────────────────────────────────────

/**
 * Create a schedule store bound to a project base path.
 *
 * Purpose: factory that closes over basePath so callers don't repeat it
 * on every operation.
 *
 * Consumer: CLI commands and autonomous mode schedulers.
 *
 * @param {string} basePath
 * @returns {{
 *   appendEntry: (scope: import("./schedule-types.js").ScheduleScope, entry: import("./schedule-types.js").ScheduleEntry) => void,
 *   readEntries: (scope: import("./schedule-types.js").ScheduleScope) => import("./schedule-types.js").ScheduleEntry[],
 *   findDue: (scope: import("./schedule-types.js").ScheduleScope, now: string|number|Date) => import("./schedule-types.js").ScheduleEntry[],
 *   findUpcoming: (scope: import("./schedule-types.js").ScheduleScope, now: string|number|Date, windowDays: number) => import("./schedule-types.js").ScheduleEntry[],
 *   _filePathForScope: (scope: import("./schedule-types.js").ScheduleScope) => string,
 * }}
 */
export function createScheduleStore(basePath) {
	return {
		appendEntry: (scope, entry) => _appendEntry(basePath, scope, entry),
		readEntries: (scope) => _readEntries(basePath, scope),
		findDue: (scope, now) => _findDue(basePath, scope, now),
		findUpcoming: (scope, now, windowDays) =>
			_findUpcoming(basePath, scope, now, windowDays),
		_filePathForScope: (scope) => _resolvePath(basePath, scope),
	};
}

/**
 * Resolve the absolute file path for a given schedule scope.
 *
 * Purpose: allow diagnostics and tests to inspect the backing file directly.
 *
 * @param {string} basePath
 * @param {import("./schedule-types.js").ScheduleScope} scope
 * @returns {string}
 */
export function resolveSchedulePath(basePath, scope) {
	return _resolvePath(basePath, scope);
}

// ─── Internal ───────────────────────────────────────────────────────────────

/**
 * @param {string} basePath
 * @param {import("./schedule-types.js").ScheduleScope} scope
 * @returns {string}
 */
function _resolvePath(basePath, scope) {
	if (scope === "global") {
		return join(sfHome(), FILENAME);
	}
	return join(sfRoot(basePath), FILENAME);
}

/**
 * @param {string} basePath
 * @param {import("./schedule-types.js").ScheduleScope} scope
 * @param {import("./schedule-types.js").ScheduleEntry} entry
 */
function _appendEntry(basePath, scope, entry) {
	ensureScheduleDb(basePath, scope);
	insertScheduleEntry(scope, {
		schemaVersion: SCHEDULE_SCHEMA_VERSION,
		...entry,
	});
}

/**
 * Read all entries from the store, skipping corrupt lines and grouping by id.
 * The latest entry (by created_at) per ID wins.
 *
 * @param {string} basePath
 * @param {import("./schedule-types.js").ScheduleScope} scope
 * @returns {import("./schedule-types.js").ScheduleEntry[]}
 */
function _readEntries(basePath, scope) {
	ensureScheduleDb(basePath, scope);
	if (countScheduleEntries(scope) > 0) {
		return getScheduleEntries(scope);
	}

	importLegacyScheduleFile(basePath, scope);
	return getScheduleEntries(scope);
}

function importLegacyScheduleFile(basePath, scope) {
	const filePath = _resolvePath(basePath, scope);
	if (!existsSync(filePath)) {
		return;
	}

	let raw;
	try {
		raw = readFileSync(filePath, "utf-8");
	} catch {
		return;
	}

	/** @type {Map<string, import("./schedule-types.js").ScheduleEntry>} */
	const byId = new Map();
	let corruptCount = 0;

	for (const line of raw.split("\n")) {
		if (!line.trim()) continue;
		try {
			/** @type {import("./schedule-types.js").ScheduleEntry} */
			const entry = normalizeScheduleEntry(JSON.parse(line));
			if (!entry || typeof entry.id !== "string") continue;

			const existing = byId.get(entry.id);
			if (!existing || entry.created_at > existing.created_at) {
				byId.set(entry.id, entry);
			}
		} catch {
			corruptCount++;
		}
	}

	if (corruptCount > 0) {
		_warn(
			`schedule-store: skipped ${corruptCount} corrupt line(s) in ${filePath}`,
		);
	}

	for (const entry of byId.values()) {
		insertScheduleEntry(scope, entry, filePath);
	}
}

function scheduleDbDir(basePath, scope) {
	if (scope === "global") return sfHome();
	return sfRoot(basePath);
}

function ensureScheduleDb(basePath, scope) {
	const dir = scheduleDbDir(basePath, scope);
	mkdirSync(dir, { recursive: true });
	openDatabase(join(dir, "sf.db"));
}

function normalizeScheduleEntry(entry) {
	if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
	const schemaVersion = entry.schemaVersion ?? SCHEDULE_SCHEMA_VERSION;
	if (schemaVersion !== SCHEDULE_SCHEMA_VERSION) return null;
	return {
		...entry,
		schemaVersion,
	};
}

/**
 * Return pending entries whose due_at is at or before `now`, sorted by due_at ASC.
 *
 * @param {string} basePath
 * @param {import("./schedule-types.js").ScheduleScope} scope
 * @param {string|number|Date} now
 * @returns {import("./schedule-types.js").ScheduleEntry[]}
 */
function _findDue(basePath, scope, now) {
	const entries = _readEntries(basePath, scope);
	const nowMs = _toTimestamp(now);
	return entries
		.filter(
			(e) =>
				(e.status === "pending" || e.status === "snoozed") &&
				_toTimestamp(e.due_at) <= nowMs,
		)
		.sort((a, b) => _toTimestamp(a.due_at) - _toTimestamp(b.due_at));
}

/**
 * Return pending entries whose due_at is after `now` and within `windowDays`,
 * sorted by due_at ASC.
 *
 * @param {string} basePath
 * @param {import("./schedule-types.js").ScheduleScope} scope
 * @param {string|number|Date} now
 * @param {number} windowDays
 * @returns {import("./schedule-types.js").ScheduleEntry[]}
 */
function _findUpcoming(basePath, scope, now, windowDays) {
	const entries = _readEntries(basePath, scope);
	const nowMs = _toTimestamp(now);
	const windowMs = windowDays * 24 * 60 * 60 * 1000;
	const cutoff = nowMs + windowMs;
	return entries
		.filter(
			(e) =>
				(e.status === "pending" || e.status === "snoozed") &&
				_toTimestamp(e.due_at) > nowMs &&
				_toTimestamp(e.due_at) <= cutoff,
		)
		.sort((a, b) => _toTimestamp(a.due_at) - _toTimestamp(b.due_at));
}

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

/**
 * Convert an ISO string, Date, or epoch ms to a numeric timestamp.
 *
 * @param {string|number|Date} value
 * @returns {number}
 */
function _toTimestamp(value) {
	if (typeof value === "number") return value;
	if (value instanceof Date) return value.getTime();
	return Date.parse(value);
}

/**
 * Write a warning to stderr without throwing.
 *
 * @param {string} message
 */
function _warn(message) {
	try {
		process.stderr.write(`[sf:schedule-store] ${message}\n`);
	} catch {
		// Best-effort warning — never break the caller.
	}
}
