import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__";
import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__";
import { showToast } from "./toast.js?v=__ASSET_VERSION__";
import { computeLedger } from "/shared/ledger.js?v=__ASSET_VERSION__";
import { buildPlanCurve } from "/shared/curve.js?v=__ASSET_VERSION__";

let roasts = [];
let openId = null;
let pollTimer = null;
let machineProfile = null;
let roasterList = [];
let planList = [];

const SVG_NS = "http://www.w3.org/2000/svg";

// Same display preference the planner's °C/°F toggle stores — data stays canonical °C,
// only labels convert.
const tempUnit = localStorage.getItem("roastPlannerTempUnit.v1") === "F" ? "F" : "C";
const displayTemp = (c) => (tempUnit === "F" ? Math.round((c * 9) / 5 + 32) : Math.round(c));

const fmtTime = (s) =>
	s == null
		? "—"
		: `${Math.floor(Math.round(s) / 60)}:${String(Math.round(s) % 60).padStart(2, "0")}`;
const fmtDate = (v) => (v ? new Date(v).toLocaleDateString() : "—");

const GRADE_LABEL = {
	excellent: "Excellent",
	good: "Good",
	fair: "Fair",
	"needs-work": "Needs work",
};

function renderTable() {
	const body = document.getElementById("roasts-body");
	if (!roasts.length) {
		body.innerHTML = `<tr><td colspan="6" class="empty-state">No finished roasts uploaded yet. Upload an Artisan .alog above, or from the planner's Reference curve drawer to attach it to a plan.</td></tr>`;
		return;
	}
	body.replaceChildren(
		...roasts.map((roast) => {
			const tr = document.createElement("tr");
			tr.style.cursor = "pointer";
			if (roast.id === openId) tr.style.background = "var(--paper-2, #f4ede6)";

			const roastCell = document.createElement("td");
			const strong = document.createElement("strong");
			strong.textContent = roast.roast?.title || roast.filename;
			const sub = document.createElement("div");
			sub.className = "muted";
			sub.style.fontSize = "11.5px";
			sub.textContent = [
				roast.roast?.roastDate || fmtDate(roast.createdAt),
				roast.roasterName,
				roast.filename,
			]
				.filter(Boolean)
				.join(" · ");
			roastCell.append(strong, sub);

			const planCell = document.createElement("td");
			planCell.textContent = roast.planTitle || "—";

			const fcCell = document.createElement("td");
			fcCell.textContent = fmtTime(roast.derived?.firstCrackS);
			const dropCell = document.createElement("td");
			dropCell.textContent = fmtTime(roast.derived?.dropS);
			const dtrCell = document.createElement("td");
			dtrCell.textContent =
				roast.derived?.dtrPct == null ? "—" : `${roast.derived.dtrPct}%`;
			const lossCell = document.createElement("td");
			lossCell.textContent =
				roast.roast?.weightLossPct == null ? "—" : `${roast.roast.weightLossPct}%`;

			tr.append(roastCell, planCell, fcCell, dropCell, dtrCell, lossCell);
			tr.addEventListener("click", () => openDetail(roast.id));
			return tr;
		}),
	);
}

async function loadRoasts() {
	try {
		roasts = (await api("/api/roasts")).roasts;
		renderTable();
		schedulePoll();
	} catch {
		document.getElementById("roasts-body").innerHTML =
			`<tr><td colspan="7" class="empty-state">Could not load roasts.</td></tr>`;
	}
}

// While any review is still pending, refresh every few seconds so the table/detail fill in as
// the LLM finishes; stops by itself once nothing is pending.
function schedulePoll() {
	clearTimeout(pollTimer);
	if (!roasts.some((roast) => roast.evaluationStatus === "pending")) return;
	pollTimer = setTimeout(async () => {
		await loadRoasts();
		if (openId) await openDetail(openId, { keepScroll: true });
	}, 4000);
}

// ---- detail view ------------------------------------------------------------

function el(tag, props = {}, text) {
	const node = document.createElement(tag);
	Object.assign(node, props);
	if (text !== undefined) node.textContent = text;
	return node;
}

function svgEl(tag, attrs) {
	const node = document.createElementNS(SVG_NS, tag);
	for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);
	return node;
}

function renderGraph(detail) {
	const svg = document.getElementById("roast-graph");
	svg.replaceChildren();
	const curve = detail.parsed?.curve ?? [];
	if (!curve.length) {
		svg.append(
			svgEl("text", { x: 357, y: 180, "text-anchor": "middle", fill: "#5a5048", "font-size": 14 }),
		);
		svg.lastChild.textContent = "No telemetry in this log.";
		return;
	}

	// Plan overlay points (same shared math as the planner page), if a plan is attached.
	let planPoints = [];
	if (detail.plan) {
		try {
			const ledger = computeLedger(detail.plan, machineProfile);
			planPoints = buildPlanCurve(detail.plan, ledger).filter(
				(p) => p.timeS != null && p.tempC != null,
			);
		} catch {
			planPoints = [];
		}
	}

	const x0 = 52,
		x1 = 692,
		y0 = 24,
		y1 = 300;
	const lastT = Math.max(
		curve[curve.length - 1].t,
		...planPoints.map((p) => p.timeS),
		540,
	);
	const tMax = Math.ceil(lastT / 60) * 60;
	const temps = curve.map((p) => p.bt).concat(planPoints.map((p) => p.tempC));
	const tempLo = Math.min(60, Math.floor((Math.min(...temps) - 10) / 20) * 20);
	const tempHi = Math.max(220, Math.ceil((Math.max(...temps) + 10) / 20) * 20);
	const xFor = (t) => x0 + (Math.max(0, Math.min(tMax, t)) / tMax) * (x1 - x0);
	const yFor = (c) => y1 - ((c - tempLo) / (tempHi - tempLo)) * (y1 - y0);

	// Grid + axis labels
	const minuteStep = tMax > 900 ? 120 : 60;
	for (let t = 0; t <= tMax; t += minuteStep) {
		svg.append(
			svgEl("line", { x1: xFor(t), y1: y0, x2: xFor(t), y2: y1, stroke: "#e5dcd2", "stroke-width": t === 0 ? 1.2 : 0.7 }),
		);
		const label = svgEl("text", { x: xFor(t), y: y1 + 16, "text-anchor": "middle", fill: "#8a7f74", "font-size": 10 });
		label.textContent = `${t / 60}`;
		svg.append(label);
	}
	for (let c = tempLo; c <= tempHi; c += 20) {
		svg.append(
			svgEl("line", { x1: x0, y1: yFor(c), x2: x1, y2: yFor(c), stroke: "#e5dcd2", "stroke-width": c === tempLo ? 1.2 : 0.7 }),
		);
		const label = svgEl("text", { x: x0 - 6, y: yFor(c) + 3, "text-anchor": "end", fill: "#8a7f74", "font-size": 10 });
		label.textContent = String(displayTemp(c));
		svg.append(label);
	}
	const xTitle = svgEl("text", { x: (x0 + x1) / 2, y: y1 + 32, "text-anchor": "middle", fill: "#8a7f74", "font-size": 10.5 });
	xTitle.textContent = "MINUTES FROM CHARGE";
	svg.append(xTitle);
	const yTitle = svgEl("text", { x: 14, y: (y0 + y1) / 2, fill: "#8a7f74", "font-size": 10.5, transform: `rotate(-90 14 ${(y0 + y1) / 2})`, "text-anchor": "middle" });
	yTitle.textContent = `BEAN TEMP °${tempUnit}`;
	svg.append(yTitle);

	// Plan curve (dashed) under the actual curve
	if (planPoints.length) {
		const d = [...planPoints]
			.sort((a, b) => a.timeS - b.timeS)
			.map((p, i) => `${i === 0 ? "M" : "L"}${xFor(p.timeS).toFixed(1)},${yFor(p.tempC).toFixed(1)}`)
			.join(" ");
		svg.append(
			svgEl("path", { d, fill: "none", stroke: "#5a5048", "stroke-width": 1.3, "stroke-dasharray": "4 3", opacity: 0.65 }),
		);
	}

	// Actual BT curve
	const d = curve
		.map((p, i) => `${i === 0 ? "M" : "L"}${xFor(p.t).toFixed(1)},${yFor(p.bt).toFixed(1)}`)
		.join(" ");
	svg.append(svgEl("path", { d, fill: "none", stroke: "#A8481A", "stroke-width": 1.8 }));

	// Milestones + turning point
	const marks = [...(detail.parsed.milestones ?? [])];
	if (detail.parsed.turningPoint)
		marks.push({ label: "TP", timeS: detail.parsed.turningPoint.timeS, tempC: detail.parsed.turningPoint.tempC });
	for (const m of marks) {
		if (m.tempC == null) continue;
		svg.append(
			svgEl("circle", { cx: xFor(m.timeS), cy: yFor(m.tempC), r: 3.4, fill: "#1a1512" }),
		);
		const label = svgEl("text", { x: xFor(m.timeS) + 6, y: yFor(m.tempC) - 6, fill: "#1a1512", "font-size": 10.5 });
		label.textContent = `${m.label} ${fmtTime(m.timeS)}`;
		svg.append(label);
	}

	// Legend
	svg.append(svgEl("line", { x1: x0 + 8, y1: 348, x2: x0 + 36, y2: 348, stroke: "#A8481A", "stroke-width": 1.8 }));
	const actualLabel = svgEl("text", { x: x0 + 42, y: 351, fill: "#5a5048", "font-size": 10.5 });
	actualLabel.textContent = "Actual BT";
	svg.append(actualLabel);
	if (planPoints.length) {
		svg.append(
			svgEl("line", { x1: x0 + 118, y1: 348, x2: x0 + 146, y2: 348, stroke: "#5a5048", "stroke-width": 1.3, "stroke-dasharray": "4 3" }),
		);
		const planLabel = svgEl("text", { x: x0 + 152, y: 351, fill: "#5a5048", "font-size": 10.5 });
		planLabel.textContent = `Plan${detail.planTitle ? `: ${detail.planTitle}` : ""}`;
		svg.append(planLabel);
	}
}

function statBlock(label, value) {
	const wrap = el("div", { className: "field" });
	wrap.append(
		el("span", { className: "field-label" }, label),
		el("div", {}, value),
	);
	return wrap;
}

function renderStats(detail) {
	const grid = document.getElementById("detail-stats");
	const derived = detail.parsed?.derived;
	const roast = detail.parsed?.roast;
	grid.replaceChildren(
		statBlock("First crack", fmtTime(derived?.firstCrackS)),
		statBlock("Development", fmtTime(derived?.developmentS)),
		statBlock("Drop", fmtTime(derived?.dropS)),
		statBlock("DTR", derived?.dtrPct == null ? "—" : `${derived.dtrPct}%`),
		statBlock("Drying share", derived?.dryingSharePct == null ? "—" : `${derived.dryingSharePct}%`),
		statBlock("Maillard share", derived?.maillardSharePct == null ? "—" : `${derived.maillardSharePct}%`),
		statBlock("Charge → out", `${roast?.weightInG ?? "—"} g → ${roast?.weightOutG ?? "—"} g`),
		statBlock("Weight loss", roast?.weightLossPct == null ? "—" : `${roast.weightLossPct}%`),
	);
}

function evaluationList(title, items) {
	const wrap = el("div");
	wrap.append(el("strong", {}, title));
	const ul = el("ul");
	ul.style.margin = "4px 0 12px";
	ul.style.paddingLeft = "18px";
	for (const item of items) {
		const li = el("li", {}, item);
		li.style.fontSize = "13px";
		ul.append(li);
	}
	wrap.append(ul);
	return wrap;
}

function renderEvaluation(detail) {
	const box = document.getElementById("detail-evaluation");
	box.replaceChildren();
	const heading = el("h3", {}, "LLM review");
	heading.style.margin = "0 0 6px";
	box.append(heading);
	if (detail.evaluationStatus === "pending") {
		box.append(el("p", { className: "field-note" }, "The LLM is reviewing this roast — this page refreshes automatically."));
		return;
	}
	if (detail.evaluationStatus === "failed") {
		const p = el(
			"p",
			{ className: "field-note" },
			`Review failed${detail.evaluationError ? ` (${detail.evaluationError})` : ""}. Use Re-evaluate to try again.`,
		);
		p.style.color = "#a8371a";
		box.append(p);
		return;
	}
	const ev = detail.evaluation;
	if (!ev) {
		box.append(el("p", { className: "field-note" }, "No review recorded for this roast."));
		return;
	}
	const grade = el("p", {}, `Grade: ${GRADE_LABEL[ev.grade] ?? ev.grade}`);
	grade.style.fontWeight = "600";
	box.append(grade, el("p", {}, ev.summary));
	if (ev.planComparison) box.append(evaluationList("Against the plan", [ev.planComparison]));
	if (ev.highlights?.length) box.append(evaluationList("What went well", ev.highlights));
	if (ev.concerns?.length) box.append(evaluationList("Concerns", ev.concerns));
	if (ev.suggestions?.length) box.append(evaluationList("Next batch", ev.suggestions));
	if (detail.parsed?.warnings?.length)
		box.append(evaluationList("Parser notes", detail.parsed.warnings));
}

async function openDetail(id, { keepScroll = false } = {}) {
	let detail;
	try {
		detail = (await api(`/api/roasts/${id}`)).roast;
	} catch (error) {
		showToast(error.message, "fail");
		return;
	}
	openId = id;
	history.replaceState(null, "", `/roasts?roast=${encodeURIComponent(id)}`);
	const card = document.getElementById("roast-detail-card");
	card.classList.remove("hidden");
	document.getElementById("detail-title").textContent =
		detail.roast?.title || detail.filename;
	const meta = document.getElementById("detail-meta");
	meta.textContent = [
		detail.roast?.roastDate || fmtDate(detail.createdAt),
		detail.roast?.roasterType,
		detail.filename,
	]
		.filter(Boolean)
		.join(" · ");
	// Plan attachment: link this roast to one of your plans (enables plan-vs-actual in the
	// review, the refine handoff, and the plan overlay on the graph).
	if (planList.length) {
		const planWrap = document.createElement("span");
		planWrap.append(" · Plan: ");
		const planSelect = document.createElement("select");
		planSelect.className = "field-input sm";
		planSelect.style.display = "inline-block";
		planSelect.style.width = "auto";
		planSelect.append(
			Object.assign(document.createElement("option"), {
				value: "",
				textContent: "— none —",
			}),
			...planList.map((plan) =>
				Object.assign(document.createElement("option"), {
					value: plan.id,
					textContent: plan.plan?.fields?.["0.1"] || "Untitled plan",
				}),
			),
		);
		planSelect.value = detail.roastPlanId ?? "";
		planSelect.addEventListener("change", async () => {
			try {
				await api(`/api/roasts/${detail.id}`, {
					method: "PUT",
					body: JSON.stringify({ roastPlanId: planSelect.value || null }),
				});
				showToast("Plan attached — re-evaluate to review against its targets.");
				await loadRoasts();
				await openDetail(detail.id, { keepScroll: true });
			} catch (error) {
				showToast(error.message, "fail");
			}
		});
		planWrap.append(planSelect);
		meta.append(planWrap);
	} else if (detail.planTitle) {
		meta.append(` · Plan: ${detail.planTitle}`);
	}
	// Machine assignment: which of the user's roasters this roast trains.
	if (roasterList.length) {
		const wrap = document.createElement("span");
		wrap.append(" · Machine: ");
		const select = document.createElement("select");
		select.className = "field-input sm";
		select.style.display = "inline-block";
		select.style.width = "auto";
		select.append(
			Object.assign(document.createElement("option"), {
				value: "",
				textContent: "— none —",
			}),
			...roasterList.map((r) =>
				Object.assign(document.createElement("option"), {
					value: r.id,
					textContent: r.name,
				}),
			),
		);
		select.value = detail.roasterId ?? "";
		select.addEventListener("change", async () => {
			try {
				await api(`/api/roasts/${detail.id}`, {
					method: "PUT",
					body: JSON.stringify({ roasterId: select.value || null }),
				});
				showToast("Machine updated — future learning uses this assignment.");
				await loadRoasts();
			} catch (error) {
				showToast(error.message, "fail");
			}
		});
		wrap.append(select);
		meta.append(wrap);
	}
	document.getElementById("detail-download").href =
		`/api/roasts/${encodeURIComponent(id)}/download`;
	document.getElementById("detail-download-updated").href =
		`/api/roasts/${encodeURIComponent(id)}/download?variant=updated`;
	renderAfterForm(detail);
	renderGraph(detail);
	renderStats(detail);
	renderEvaluation(detail);
	renderTable();
	if (!keepScroll) card.scrollIntoView({ behavior: "smooth", block: "start" });
}

// ---- after the roast (moved here from the plan worksheet) -------------------

const AFTER_FIELDS = [
	"greenIn", "out", "weightLossPct", "vsTarget", "actualDtrPct", "colour",
	"restedDays", "brewRatio", "method", "cupNotes", "oneChange", "disproof",
];

function renderAfterForm(detail) {
	const form = document.getElementById("after-form");
	const after = detail.after ?? {};
	// Prefill measurable values straight from the .alog so the user only types what the
	// machine couldn't know (colour, cup notes, the one change).
	const auto = {
		greenIn: detail.parsed?.roast?.weightInG || "",
		out: detail.parsed?.roast?.weightOutG || "",
		weightLossPct: detail.parsed?.roast?.weightLossPct ?? "",
		actualDtrPct: detail.parsed?.derived?.dtrPct ?? "",
	};
	for (const field of AFTER_FIELDS)
		form[field].value = after[field] ?? (auto[field] === 0 ? "" : (auto[field] ?? ""));
}

document.getElementById("after-form").addEventListener("submit", async (event) => {
	event.preventDefault();
	if (!openId) return;
	const form = event.target;
	const after = {};
	for (const field of AFTER_FIELDS) after[field] = form[field].value;
	const button = document.getElementById("after-save");
	button.disabled = true;
	try {
		await api(`/api/roasts/${openId}`, {
			method: "PUT",
			body: JSON.stringify({ after }),
		});
		showToast("After-roast saved — included in the updated .alog.");
	} catch (error) {
		showToast(error.message, "fail");
	} finally {
		button.disabled = false;
	}
});

// Open (or create) the cupping session for this roast's plan — same flow the planner offers.
document.getElementById("open-cupping").addEventListener("click", async (event) => {
	const button = event.currentTarget;
	if (!openId || button.disabled) return;
	const roast = roasts.find((r) => r.id === openId);
	const planId = roast?.roastPlanId;
	if (!planId) {
		showToast("Attach a plan to this roast first — cupping sessions hang off plans.", "fail");
		return;
	}
	button.disabled = true;
	try {
		const existing = await api(`/api/cupping?plan=${encodeURIComponent(planId)}`).catch(() => ({ sessions: [] }));
		if (existing.sessions?.length) {
			location.assign(`/cupping?session=${existing.sessions[0].id}`);
			return;
		}
		const created = await api("/api/cupping", {
			method: "POST",
			body: JSON.stringify({ roastPlanId: planId }),
		});
		location.assign(`/cupping?session=${created.session.id}`);
	} catch (error) {
		showToast(error.message, "fail");
	} finally {
		button.disabled = false;
	}
});

function closeDetail() {
	openId = null;
	history.replaceState(null, "", "/roasts");
	document.getElementById("roast-detail-card").classList.add("hidden");
	renderTable();
}

// ---- uploads ----------------------------------------------------------------

async function uploadFiles(files) {
	const status = document.getElementById("upload-status");
	status.hidden = false;
	let uploaded = 0;
	for (const file of files) {
		status.textContent = `Uploading ${file.name}…`;
		try {
			await api("/api/roasts", {
				method: "POST",
				body: JSON.stringify({ filename: file.name, content: await file.text() }),
			});
			uploaded++;
		} catch (error) {
			showToast(`${file.name}: ${error.message}`, "fail");
		}
	}
	status.textContent = uploaded
		? `Uploaded ${uploaded} roast${uploaded === 1 ? "" : "s"} — the LLM review runs in the background.`
		: "";
	status.hidden = !status.textContent;
	await loadRoasts();
}

document.getElementById("roast-upload").addEventListener("change", async (event) => {
	const files = [...(event.target.files ?? [])];
	event.target.value = "";
	if (files.length) await uploadFiles(files);
});

document.getElementById("detail-close").addEventListener("click", closeDetail);
document.getElementById("detail-reevaluate").addEventListener("click", async (event) => {
	if (!openId || event.currentTarget.disabled) return;
	const button = event.currentTarget;
	button.disabled = true;
	try {
		await api(`/api/roasts/${openId}/evaluate`, { method: "POST" });
		showToast("Re-evaluation queued.");
		await loadRoasts();
		await openDetail(openId, { keepScroll: true });
	} catch (error) {
		showToast(error.message, "fail");
	} finally {
		button.disabled = false;
	}
});
document.getElementById("detail-delete").addEventListener("click", async () => {
	if (!openId) return;
	const roast = roasts.find((r) => r.id === openId);
	const name = roast?.roast?.title || roast?.filename || "this roast";
	if (!confirm(`Delete "${name}" and its stored .alog? This cannot be undone.`)) return;
	try {
		await api(`/api/roasts/${openId}`, { method: "DELETE" });
		showToast("Roast deleted.");
		closeDetail();
		await loadRoasts();
	} catch (error) {
		showToast(error.message, "fail");
	}
});

document.getElementById("btn-logout").addEventListener("click", async () => {
	await protectedFetch("/api/auth/logout", { method: "POST" });
	location.assign("/");
});

async function init() {
	initSideNav();
	const user = await loadNavUser();
	if (!user) return;
	// Same learned pace the planner uses, so the dashed plan overlay matches the planner's curve.
	try {
		machineProfile = (await api("/api/machine-profile")).profile;
	} catch {
		machineProfile = null;
	}
	try {
		roasterList = (await api("/api/roasters")).roasters;
	} catch {
		roasterList = [];
	}
	try {
		planList = (await api("/api/plans")).plans;
	} catch {
		planList = [];
	}
	await loadRoasts();
	const requested = new URLSearchParams(location.search).get("roast");
	if (requested && roasts.some((roast) => roast.id === requested))
		await openDetail(requested, { keepScroll: true });
}

init();
