/**
 * SF Command — /schedule
 *
 * Schedule management: add, list, done, cancel, snooze, run.
 * Entries are stored in SQLite (`schedule_entries`). Legacy schedule JSONL is
 * imported on first read when the DB has no schedule rows.
 */

import {
	executeProjectScheduleCommand,
	markProjectScheduleDone,
} from "./schedule/schedule-autonomous-dispatch.js";
import { createScheduleStore } from "./schedule/schedule-store.js";
import { ALL_SCHEDULE_KINDS, isValidKind } from "./schedule/schedule-types.js";
import { generateULID } from "./schedule/schedule-ulid.js";

// ─── Duration parser ────────────────────────────────────────────────────────

/**
 * Parse a duration string into milliseconds.
 *
 * Purpose: convert human-friendly durations like "2w" or "30m" into
 * epoch milliseconds for computing due_at timestamps.
 *
 * Consumer: schedule add and snooze commands.
 *
 * @param {string} str
 * @returns {number} milliseconds
 */
export function parseDuration(str) {
	const match = str.trim().match(/^(\d+)([wdhm])$/i);
	if (!match) {
		throw new Error(
			`Invalid duration: "${str}". Expected format: <number><unit> where unit is w(weeks), d(days), h(hours), m(minutes).`,
		);
	}
	const value = parseInt(match[1], 10);
	const unit = match[2].toLowerCase();
	switch (unit) {
		case "w":
			return value * 7 * 24 * 60 * 60 * 1000;
		case "d":
			return value * 24 * 60 * 60 * 1000;
		case "h":
			return value * 60 * 60 * 1000;
		case "m":
			return value * 60 * 1000;
		default:
			throw new Error(`Unknown duration unit: ${unit}`);
	}
}

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

function _basePath() {
	return process.cwd();
}

function _isoRelative(dueAt) {
	const now = Date.now();
	const due = new Date(dueAt).getTime();
	const diff = due - now;
	const abs = Math.abs(diff);
	const days = Math.floor(abs / (24 * 60 * 60 * 1000));
	const hours = Math.floor((abs % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
	if (diff < 0) {
		if (days > 0) return `overdue ${days}d`;
		if (hours > 0) return `overdue ${hours}h`;
		return "overdue";
	}
	if (days > 0) return `${days}d`;
	if (hours > 0) return `${hours}h`;
	return "soon";
}

function _formatTable(rows) {
	if (rows.length === 0) return "";
	const colCount = rows[0].length;
	const widths = new Array(colCount).fill(0);
	for (const row of rows) {
		for (let i = 0; i < colCount; i++) {
			widths[i] = Math.max(widths[i], String(row[i]).length);
		}
	}
	return rows
		.map((row) =>
			row.map((cell, i) => String(cell).padEnd(widths[i])).join("  "),
		)
		.join("\n");
}

function _findEntry(store, scope, idPrefix) {
	const entries = store.readEntries(scope);
	return {
		entry: entries.find((e) => e.id.startsWith(idPrefix)) ?? null,
		entries,
	};
}

function _splitArgs(args) {
	if (Array.isArray(args)) {
		return args.map((part) => String(part)).filter(Boolean);
	}
	const input = String(args ?? "");
	const tokens = [];
	let current = "";
	let quote = null;
	let escaped = false;

	for (const char of input) {
		if (escaped) {
			current += char;
			escaped = false;
			continue;
		}
		if (char === "\\") {
			escaped = true;
			continue;
		}
		if (quote) {
			if (char === quote) {
				quote = null;
			} else {
				current += char;
			}
			continue;
		}
		if (char === "'" || char === '"') {
			quote = char;
			continue;
		}
		if (/\s/.test(char)) {
			if (current) {
				tokens.push(current);
				current = "";
			}
			continue;
		}
		current += char;
	}
	if (escaped) current += "\\";
	if (current) tokens.push(current);
	return tokens;
}

function _joinPlain(parts) {
	return parts.join(" ").trim();
}

function _shellQuote(part) {
	if (/^[A-Za-z0-9_./:=@%+-]+$/.test(part)) return part;
	return `'${part.replace(/'/g, "'\\''")}'`;
}

function _commandFromParts(parts) {
	return parts
		.map((part) => _shellQuote(String(part)))
		.join(" ")
		.trim();
}

// ─── Subcommands ────────────────────────────────────────────────────────────

async function addItem(args, ctx) {
	const parts = _splitArgs(args);

	let kind = "reminder";
	let scope = "project";
	let dueAt = null;
	let autonomousDispatch = false;
	let capture = null;
	const titleParts = [];

	for (let i = 0; i < parts.length; i++) {
		const p = parts[i];
		if (p === "--") {
			titleParts.push(...parts.slice(i + 1));
			break;
		}
		if (p === "--kind" || p === "-k") {
			kind = parts[++i];
			continue;
		}
		if (p === "--scope" || p === "-s") {
			scope = parts[++i];
			continue;
		}
		if (p === "--in") {
			const dur = parts[++i];
			try {
				dueAt = new Date(Date.now() + parseDuration(dur)).toISOString();
			} catch (err) {
				ctx.ui.notify(err.message, "warning");
				return;
			}
			continue;
		}
		if (p === "--at") {
			const iso = parts[++i];
			const parsed = Date.parse(iso);
			if (Number.isNaN(parsed)) {
				ctx.ui.notify(`Invalid ISO date: ${iso}`, "warning");
				return;
			}
			dueAt = new Date(parsed).toISOString();
			continue;
		}
		if (p === "--autonomous-dispatch") {
			autonomousDispatch = true;
			continue;
		}
		if (p === "--auto-dispatch" || p === "--auto") {
			ctx.ui.notify(
				"Unsupported schedule argument: use --autonomous-dispatch.",
				"warning",
			);
			return;
		}
		if (p === "--capture") {
			capture = parts[++i];
			continue;
		}
		titleParts.push(p);
	}

	if (!isValidKind(kind)) {
		ctx.ui.notify(
			`Unknown kind: ${kind}. Valid: ${ALL_SCHEDULE_KINDS.join(", ")}`,
			"warning",
		);
		return;
	}
	if (capture && capture !== "stdout") {
		ctx.ui.notify(`Unknown capture mode: ${capture}. Valid: stdout`, "warning");
		return;
	}
	if (scope !== "project" && scope !== "global") {
		ctx.ui.notify(`Unknown scope: ${scope}. Valid: project, global`, "warning");
		return;
	}
	if (!dueAt) {
		ctx.ui.notify(
			"Usage: /schedule add --in <duration> <title>\n       /schedule add --at <ISO-date> <title>",
			"warning",
		);
		return;
	}

	const title =
		kind === "command" ? _commandFromParts(titleParts) : _joinPlain(titleParts);
	if (!title) {
		ctx.ui.notify(
			"Missing title. Example: /schedule add --in 2w 'Review adoption metrics'",
			"warning",
		);
		return;
	}

	const store = createScheduleStore(_basePath());
	const entry = {
		id: generateULID(),
		kind,
		status: "pending",
		due_at: dueAt,
		created_at: new Date().toISOString(),
		payload: _payloadForKind(kind, title, capture),
		created_by: "user",
		...(autonomousDispatch ? { autonomous_dispatch: true } : {}),
	};
	store.appendEntry(scope, entry);
	ctx.ui.notify(`Scheduled: ${entry.id}\nDue: ${entry.due_at}`, "success");
}

function _payloadForKind(kind, title, capture) {
	if (kind === "command") {
		return {
			command: title,
			...(capture === "stdout" ? { capture } : {}),
		};
	}
	if (kind === "prompt") {
		return { prompt: title, message: title };
	}
	return { message: title };
}

async function listItems(args, ctx) {
	const parts = _splitArgs(args);
	let scope = "project";
	let showDueOnly = false;
	let showAll = false;
	let json = false;

	for (let i = 0; i < parts.length; i++) {
		const p = parts[i];
		if (p === "--scope" || p === "-s") {
			scope = parts[++i];
			continue;
		}
		if (p === "--due" || p === "-d") {
			showDueOnly = true;
			continue;
		}
		if (p === "--all" || p === "-a") {
			showAll = true;
			continue;
		}
		if (p === "--json" || p === "-j") {
			json = true;
		}
	}

	if (scope !== "project" && scope !== "global") {
		ctx.ui.notify(`Unknown scope: ${scope}. Valid: project, global`, "warning");
		return;
	}

	const store = createScheduleStore(_basePath());
	const now = new Date().toISOString();

	let entries;
	if (showDueOnly) {
		entries = store.findDue(scope, now);
	} else if (showAll) {
		entries = store.readEntries(scope);
	} else {
		entries = store
			.readEntries(scope)
			.filter((e) => e.status === "pending" || e.status === "snoozed");
	}

	entries.sort((a, b) => new Date(a.due_at) - new Date(b.due_at));

	if (json) {
		ctx.ui.notify(JSON.stringify(entries, null, 2), "info");
		return;
	}

	if (entries.length === 0) {
		ctx.ui.notify("No scheduled items.", "info");
		return;
	}

	const rows = [["ID", "Title", "Due", "Status", "Kind"]];
	for (const e of entries) {
		const title = e.payload?.message || e.payload?.command || e.id;
		rows.push([
			e.id.slice(0, 8),
			title.slice(0, 40),
			_isoRelative(e.due_at),
			e.status,
			e.kind,
		]);
	}
	ctx.ui.notify(_formatTable(rows), "info");
}

async function markDone(args, ctx) {
	const parts = _splitArgs(args);
	let idPrefix = "";
	let scope = "project";

	for (let i = 0; i < parts.length; i++) {
		const p = parts[i];
		if (p === "--scope" || p === "-s") {
			scope = parts[++i];
			continue;
		}
		if (!idPrefix) {
			idPrefix = p;
		}
	}

	if (!idPrefix) {
		ctx.ui.notify("Usage: /schedule done [--scope <scope>] <id>", "warning");
		return;
	}

	if (scope !== "project" && scope !== "global") {
		ctx.ui.notify(`Unknown scope: ${scope}. Valid: project, global`, "warning");
		return;
	}

	const store = createScheduleStore(_basePath());
	const { entry } = _findEntry(store, scope, idPrefix);
	if (!entry) {
		ctx.ui.notify(`Item ${idPrefix} not found in ${scope} scope.`, "warning");
		return;
	}
	const updated = {
		...entry,
		status: "done",
		created_at: new Date().toISOString(),
	};
	store.appendEntry(scope, updated);
	ctx.ui.notify(`Marked done: ${entry.id}`, "success");
}

async function markCancel(args, ctx) {
	const parts = _splitArgs(args);
	let idPrefix = "";
	let scope = "project";

	for (let i = 0; i < parts.length; i++) {
		const p = parts[i];
		if (p === "--scope" || p === "-s") {
			scope = parts[++i];
			continue;
		}
		if (!idPrefix) {
			idPrefix = p;
		}
	}

	if (!idPrefix) {
		ctx.ui.notify("Usage: /schedule cancel [--scope <scope>] <id>", "warning");
		return;
	}

	if (scope !== "project" && scope !== "global") {
		ctx.ui.notify(`Unknown scope: ${scope}. Valid: project, global`, "warning");
		return;
	}

	const store = createScheduleStore(_basePath());
	const { entry } = _findEntry(store, scope, idPrefix);
	if (!entry) {
		ctx.ui.notify(`Item ${idPrefix} not found in ${scope} scope.`, "warning");
		return;
	}
	const updated = {
		...entry,
		status: "cancelled",
		created_at: new Date().toISOString(),
	};
	store.appendEntry(scope, updated);
	ctx.ui.notify(`Cancelled: ${entry.id}`, "success");
}

async function snoozeItem(args, ctx) {
	const parts = _splitArgs(args);
	let idPrefix = "";
	let by = "";
	let scope = "project";

	for (let i = 0; i < parts.length; i++) {
		if (parts[i] === "--by" || parts[i] === "-b") {
			by = parts[++i];
		} else if (parts[i] === "--scope" || parts[i] === "-s") {
			scope = parts[++i];
		} else if (!idPrefix) {
			idPrefix = parts[i];
		}
	}

	if (!idPrefix || !by) {
		ctx.ui.notify(
			"Usage: /schedule snooze [--scope <scope>] <id> --by <duration>",
			"warning",
		);
		return;
	}

	if (scope !== "project" && scope !== "global") {
		ctx.ui.notify(`Unknown scope: ${scope}. Valid: project, global`, "warning");
		return;
	}

	const store = createScheduleStore(_basePath());
	const { entry } = _findEntry(store, scope, idPrefix);
	if (!entry) {
		ctx.ui.notify(`Item ${idPrefix} not found in ${scope} scope.`, "warning");
		return;
	}

	let offsetMs;
	try {
		offsetMs = parseDuration(by);
	} catch (err) {
		ctx.ui.notify(err.message, "warning");
		return;
	}

	const now = new Date().toISOString();
	const newDue = new Date(
		new Date(entry.due_at).getTime() + offsetMs,
	).toISOString();
	const updated = {
		...entry,
		status: "pending",
		due_at: newDue,
		created_at: now,
		snoozed_at: now,
	};
	store.appendEntry(scope, updated);
	ctx.ui.notify(`Snoozed: ${entry.id}\nNew due: ${newDue}`, "success");
}

async function runItem(args, ctx) {
	const parts = _splitArgs(args);
	let idPrefix = "";
	let scope = "project";
	let dryRun = false;

	for (const part of parts) {
		if (part === "--dry-run" || part === "--dry") {
			dryRun = true;
		} else if (part === "--scope" || part === "-s") {
			scope = parts[parts.indexOf(part) + 1];
		} else if (!idPrefix) {
			idPrefix = part;
		}
	}

	if (!idPrefix) {
		ctx.ui.notify(
			"Usage: /schedule run [--scope <scope>] [--dry-run] <id>",
			"warning",
		);
		return;
	}

	if (scope !== "project" && scope !== "global") {
		ctx.ui.notify(`Unknown scope: ${scope}. Valid: project, global`, "warning");
		return;
	}

	const store = createScheduleStore(_basePath());
	const { entry } = _findEntry(store, scope, idPrefix);
	if (!entry) {
		ctx.ui.notify(`Item ${idPrefix} not found in ${scope} scope.`, "warning");
		return;
	}

	const kind = entry.kind;
	const payload = entry.payload || {};

	switch (kind) {
		case "reminder":
		case "milestone_check":
		case "review_due": {
			const title = payload.message || entry.id;
			ctx.ui.notify(`Reminder: ${title}`, "info");
			break;
		}
		case "command": {
			const command = payload.command;
			if (dryRun) {
				ctx.ui.notify(
					JSON.stringify(
						{
							id: entry.id,
							kind: entry.kind,
							status: entry.status,
							scope: scope,
							cwd: scope === "project" ? _basePath() : undefined,
							command,
							autonomous_dispatch: entry.autonomous_dispatch === true,
							would_execute: typeof command === "string" && command.length > 0,
						},
						null,
						2,
					),
					"info",
				);
				return;
			}

			// Global scope commands cannot execute (no repo context)
			if (scope === "global") {
				ctx.ui.notify(
					`Cannot execute global scope command: ${entry.id}\n` +
						`Global commands can only be run in project context.\n` +
						`Use project scope for executable commands.`,
					"warning",
				);
				return;
			}

			const result = executeProjectScheduleCommand(_basePath(), entry);
			if (!result.ok) {
				ctx.ui.notify(`Command failed: ${result.reason}`, "error");
				return;
			}
			if (result.stdout) ctx.ui.notify(result.stdout, "info");
			ctx.ui.notify(`Completed: ${entry.id}`, "success");
			return;
		}
		case "prompt": {
			const title = payload.prompt || payload.message || entry.id;
			if (dryRun) {
				ctx.ui.notify(`Dry run prompt: ${title}`, "info");
				return;
			}
			ctx.ui.notify(`Prompt: ${title}`, "info");
			break;
		}
		default: {
			ctx.ui.notify(`Unknown kind "${kind}" for item ${entry.id}.`, "warning");
			return;
		}
	}

	// Mark done on success
	markProjectScheduleDone(_basePath(), entry);
	ctx.ui.notify(`Completed: ${entry.id}`, "success");
}

// ─── Public handler ─────────────────────────────────────────────────────────

/**
 * Handle /schedule subcommands.
 *
 * Purpose: route schedule CLI input to the appropriate subcommand.
 *
 * Consumer: commands dispatcher (dispatcher.js).
 *
 * @param {string|string[]} args
 * @param {import("@singularity-forge/coding-agent").ExtensionContext} ctx
 */
export async function handleSchedule(args, ctx) {
	const parts = _splitArgs(args);
	const sub = parts[0] ?? "";
	const rest = Array.isArray(args) ? parts.slice(1) : parts.slice(1).join(" ");

	switch (sub) {
		case "add":
			return addItem(rest, ctx);
		case "list":
			return listItems(rest, ctx);
		case "done":
			return markDone(rest, ctx);
		case "cancel":
			return markCancel(rest, ctx);
		case "snooze":
			return snoozeItem(rest, ctx);
		case "run":
			return runItem(rest, ctx);
		case "":
			ctx.ui.notify(
				"Usage: /schedule add|list|done|cancel|snooze|run\n" +
					"  add   --in \u003cduration\u003e [--kind \u003ckind\u003e] [--scope \u003cscope\u003e] [--autonomous-dispatch] \u003ctitle-or-command\u003e\n" +
					"  list  [--due] [--all] [--json] [--scope \u003cscope\u003e]\n" +
					"  done  [--scope \u003cscope\u003e] \u003cid\u003e\n" +
					"  cancel [--scope \u003cscope\u003e] \u003cid\u003e\n" +
					"  snooze [--scope \u003cscope\u003e] \u003cid\u003e --by \u003cduration\u003e\n" +
					"  run   [--scope \u003cscope\u003e] [--dry-run] \u003cid\u003e",
				"info",
			);
			return;
		default:
			ctx.ui.notify(
				`Unknown schedule subcommand: ${sub}. Use /schedule for usage.`,
				"warning",
			);
	}
}
