// BrewPi browser client (v1: Manual + Progress tabs only - see README.md's
// "Browser client" section). Talks the exact same WebSocket pub/sub
// protocol as client/brewpi_gui.py - this file mirrors that client's logic
// directly wherever the two overlap, so see brewpi_gui.py's own comments
// for the *why* behind anything that looks non-obvious here.

const CHANNELS = ['Pot', 'Sensor', 'Heater', 'Stirrer', 'TempCtrl', 'Sud', 'System'];
// str(SudState.X) values (as sent in the 'State' message) that count as
// "running" - mirrors client/brewpi_gui.py's SUD_RUNNING_STATES.
const RUNNING_STATES = new Set(['SudState.RAMPING', 'SudState.HOLDING', 'SudState.WAIT_USER']);
const PAUSED_STATE = 'SudState.PAUSED';
const WAIT_USER_STATE = 'SudState.WAIT_USER';

let ws = null;
let connected = false;

// Live Manual-tab readouts (TempCtrl/Heater/Stirrer channels).
let plotTempIst = null;
let stirrerSpeedIst = 0;
// Only the very first authoritative read after connecting is used to set
// these particular sliders - thereafter they're purely user-controlled.
// Without this, a slider mid-drag would keep getting snapped back by the
// server's own *actual* (not commanded) readback - e.g. the heater's
// discrete power steps/duty-cycling mean 'Power' rarely matches whatever
// was just commanded exactly. Mirrors brewpi_gui.py's *_initial_update
// flags (there, needed to avoid echoing a command right back as the same
// value via Qt's signal/slot machinery; here the DOM itself never echoes
// a plain .value assignment as an 'input'/'change' event, but the
// stale-overwrite risk is the same).
let initialPowerSync = true;
let initialSpeedSync = true;
let initialHeatrateSync = true;
let initialTempSollSync = true;
let initialHeaterActivateSync = true;
let initialStirrerActivateSync = true;

let ambientTemp = null;
let warpFactor = 1.0;

// Sud state - mirrors the sud_* fields on client/brewpi_gui.py's Window.
let sudSchedule = [];
let sudName = '';
let sudPotMass = 0;
let sudGrainMass = 0;
let sudWaterMass = 0;
let sudVolumen = 0;
let sudEmpty = true;
let sudLastLoadedDoc = null;
let sudState = null;
let wasRunning = false;
let sudStepIndex = null;
let sudStepDescr = null;
let sudStepType = null;
let sudHoldRemaining = 0;
let sudUserMessage = null;
// Never reset back to "unknown" just because a run finishes - see
// client/brewpi_gui.py's sud_elapsed_last for why (sud_elapsed_seconds
// there gates a forecast-plot view this client doesn't have, so that
// distinction doesn't apply here - this is simply always "the last real
// Elapsed value").
let elapsed = null;
let stepActualFrozen = {};
let forecastStepStarts = {};
let forecastFinished = false;
let forecastFinalT = null;
let energyByStep = {};
let energyCurrent = 0;
// Set right before sending {Sud: {Save: true}} - the next 'Json' push is
// then this request's reply (not a schedule update to render), so it
// triggers a file download instead - mirrors client/brewpi_gui.py's
// sud_save_path flag (on_action_sud_save()/on_sud_changed()'s 'Json'
// branch).
let sudSavePending = false;

let stepPlates = [];
let blinkOn = true;

function sendMsg(channel, payload) {
	if (ws && ws.readyState === WebSocket.OPEN) {
		ws.send(JSON.stringify({[channel]: payload}));
	}
}

function formatDuration(seconds) {
	seconds = Math.max(0, Math.floor(seconds));
	const hours = Math.floor(seconds / 3600);
	const minutes = Math.floor((seconds % 3600) / 60);
	const secs = seconds % 60;
	if (hours) {
		return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
	}
	return `${minutes}:${String(secs).padStart(2, '0')}`;
}

// --- Sud schedule resolution - ports components/sud.py's _merge_defaults()/
// _build_step() verbatim, since the raw doc from 'Json' is unresolved. ---

function isPlainObject(value) {
	return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function mergeDefaults(defaults, override) {
	const merged = Object.assign({}, defaults);
	for (const [key, value] of Object.entries(override)) {
		if (isPlainObject(merged[key]) && isPlainObject(value)) {
			merged[key] = mergeDefaults(merged[key], value);
		} else {
			merged[key] = value;
		}
	}
	return merged;
}

function buildStep(number, defaults, rawStep) {
	const step = {number: number};
	for (const key of ['descr', 'user_message', 'user_wait_for_continue', 'grain_mass', 'water_mass']) {
		step[key] = (key in rawStep) ? rawStep[key] : defaults[key];
	}
	// Deliberately not defaulted from default.step - a step omitting it
	// has no new target of its own and keeps whatever the previous step
	// left running (see components/sud.py's _build_step()).
	step.temperature = ('temperature' in rawStep) ? rawStep.temperature : null;
	step.ramp = mergeDefaults(defaults.ramp || {}, rawStep.ramp || {});
	if ('hold' in rawStep) {
		step.hold = mergeDefaults(defaults.hold || {}, rawStep.hold || {});
	}
	return step;
}

function parseSudDoc(doc) {
	const pot = doc.pot || {};
	const stepDefaults = (doc.default && doc.default.step) || {};
	const schedule = (doc.steps || []).map((raw, i) => buildStep(i + 1, stepDefaults, raw));
	return {
		name: doc.Name || '',
		schedule: schedule,
		potMass:   pot.mass        || 0,
		grainMass: pot.grain_mass  || 0,
		waterMass: pot.water_mass  || 0,
		volumen:   pot.volumen     || 0,
	};
}

// --- Progress tab: one step-plate per schedule step - mirrors
// client/brewpi_gui.py's StepPlate/_rebuild_step_plates()/
// _update_step_plates(). ---

function createStepPlate(index, step, resolvedTemp) {
	const el = document.createElement('div');
	el.className = 'step-plate';

	const led = document.createElement('div');
	led.className = 'led';
	const title = document.createElement('div');
	title.className = 'title';
	title.textContent = `Step ${index + 1}: ${step.descr || ''}`;
	const remaining = document.createElement('div');
	remaining.className = 'remaining';
	remaining.textContent = '--:--';
	const target = document.createElement('div');
	target.className = 'field span2';
	const actual = document.createElement('div');
	actual.className = 'field';
	const masses = document.createElement('div');
	masses.className = 'field span2';
	const energy = document.createElement('div');
	energy.className = 'field';
	const rate = document.createElement('div');
	rate.className = 'field span2';
	const stirrer = document.createElement('div');
	stirrer.className = 'field';
	el.append(led, title, remaining, target, actual, masses, energy, rate, stirrer);

	const grain = step.grain_mass || 0;
	const water = step.water_mass || 0;
	masses.textContent = `Grain ${grain.toFixed(2)} kg / Water ${water.toFixed(2)} kg`;
	rate.textContent = `Rate ${((step.ramp && step.ramp.rate) || 0).toFixed(2)} °/min`;
	target.textContent = `Target ${resolvedTemp !== null ? resolvedTemp.toFixed(1) + ' °C' : '—'}`;
	// The hold phase's speed, not the ramp phase's, as the "configured"
	// fallback shown for an inactive step - see StepPlate.set_step()'s
	// own comment.
	const holdStirrer = (step.hold && step.hold.stirrer) || {};
	const rampStirrer = (step.ramp && step.ramp.stirrer) || {};
	const configuredSpeed = ('speed' in holdStirrer) ? holdStirrer.speed : (rampStirrer.speed || 0);

	return {el, led, remaining, target, actual, masses, energy, rate, stirrer, ledMode: 'off', configuredSpeed};
}

function rebuildStepPlates() {
	const container = document.getElementById('step-plates');
	container.innerHTML = '';
	stepPlates = [];
	let resolvedTemp = null;
	sudSchedule.forEach((step, index) => {
		if (step.temperature !== null && step.temperature !== undefined) {
			resolvedTemp = step.temperature;
		}
		const plate = createStepPlate(index, step, resolvedTemp);
		stepPlates.push(plate);
		container.appendChild(plate.el);
	});
	updateStepPlates();
}

function applyLedColor(plate) {
	if (plate.ledMode === 'done') {
		plate.led.style.background = 'var(--led-green)';
	} else if (plate.ledMode === 'ramp') {
		plate.led.style.background = blinkOn ? 'var(--led-orange)' : 'var(--led-orange-dim)';
	} else if (plate.ledMode === 'hold') {
		plate.led.style.background = blinkOn ? 'var(--led-green)' : 'var(--led-green-dim)';
	} else {
		plate.led.style.background = 'var(--led-off)';
	}
}

function setLed(plate, mode) {
	plate.ledMode = mode;
	applyLedColor(plate);
}

function updateStepPlates() {
	if (stepPlates.length === 0) {
		return;
	}
	const currentElapsed = (elapsed !== null) ? elapsed : 0;
	const n = stepPlates.length;
	stepPlates.forEach((plate, i) => {
		const startT = forecastStepStarts[i];
		let endT = forecastStepStarts[i + 1];
		if (endT === undefined && i === n - 1 && forecastFinished) {
			endT = forecastFinalT;
		}
		if (startT !== undefined && endT !== undefined) {
			const total = Math.max(0, endT - startT);
			plate.remaining.textContent = formatDuration(Math.max(0, Math.min(total, endT - currentElapsed)));
		} else {
			plate.remaining.textContent = '--:--';
		}

		if (sudStepIndex === null || i > sudStepIndex) {
			setLed(plate, 'off');
		} else if (i < sudStepIndex) {
			setLed(plate, 'done');
		} else {
			// WAIT_USER/PAUSED don't change sudStepType - it stays
			// whatever phase was actually last active, which is exactly
			// the color this step's LED should keep flashing.
			setLed(plate, (sudStepType === 'ramp' || sudStepType === 'hold') ? sudStepType : 'hold');
		}

		if (sudStepIndex !== null && i === sudStepIndex) {
			plate.actual.textContent = `Actual ${plotTempIst !== null ? plotTempIst.toFixed(1) + ' °C' : '—'}`;
			plate.stirrer.textContent = `Stirrer ${stirrerSpeedIst.toFixed(0)} rpm`;
			plate.energy.textContent = `Energy ${energyCurrent.toFixed(0)} Wh`;
		} else if (i in stepActualFrozen) {
			plate.actual.textContent = `Actual ${stepActualFrozen[i].toFixed(1)} °C`;
			plate.stirrer.textContent = `Stirrer ${plate.configuredSpeed.toFixed(0)} rpm (cfg)`;
			plate.energy.textContent = (i in energyByStep) ? `Energy ${energyByStep[i].toFixed(0)} Wh` : 'Energy —';
		} else {
			plate.actual.textContent = 'Actual —';
			plate.stirrer.textContent = `Stirrer ${plate.configuredSpeed.toFixed(0)} rpm (cfg)`;
			plate.energy.textContent = (i in energyByStep) ? `Energy ${energyByStep[i].toFixed(0)} Wh` : 'Energy —';
		}
	});
}

// --- Pot visualization ---

function wavePathD(x1, x2, y, amplitude, wavelength) {
	let d = `M ${x1} ${y.toFixed(1)}`;
	for (let x = x1 + 3; x <= x2; x += 3) {
		const yw = y + amplitude * Math.sin(2 * Math.PI * (x - x1) / wavelength);
		d += ` L ${x} ${yw.toFixed(1)}`;
	}
	return d;
}

function currentStepWaterMass() {
	if (sudStepIndex !== null && sudStepIndex >= 0 && sudStepIndex < sudSchedule.length) {
		return sudSchedule[sudStepIndex].water_mass || 0;
	}
	return sudWaterMass;
}

function updatePotVisualization() {
	// Thermometer mercury column (0°C = y=174, 100°C = y=28, tube length = 146)
	const tubeBottom = 174;
	const tubeLen = 146;
	const temp = plotTempIst !== null ? Math.max(0, Math.min(100, plotTempIst)) : 0;
	const mercuryH = (temp / 100) * tubeLen;
	document.getElementById('thermo-mercury').setAttribute('y', (tubeBottom - mercuryH).toFixed(1));
	document.getElementById('thermo-mercury').setAttribute('height', mercuryH.toFixed(1));
	document.getElementById('thermo-temp-label').textContent =
		plotTempIst !== null ? plotTempIst.toFixed(1) + ' °C' : '--';

	// Water fill
	const waterFillEl = document.getElementById('water-fill');
	const waterWaveEl = document.getElementById('water-wave');
	const potInnerTop = 30;
	const potInnerBottom = 172;
	const potInnerH = potInnerBottom - potInnerTop; // 142

	if (sudEmpty || sudVolumen <= 0) {
		waterFillEl.setAttribute('height', '0');
		waterWaveEl.setAttribute('d', '');
		return;
	}

	const fillFraction = Math.min(1, Math.max(0, currentStepWaterMass() / sudVolumen));
	const waterH = fillFraction * potInnerH;
	const waterTop = potInnerBottom - waterH;
	waterFillEl.setAttribute('y', waterTop.toFixed(1));
	waterFillEl.setAttribute('height', waterH.toFixed(1));
	waterWaveEl.setAttribute('d', wavePathD(63, 177, waterTop, 3, 28));
}

// --- Status line - mirrors update_status_step_label(). ---

function updateStatusLine() {
	const el = document.getElementById('sud-status-line');
	if (sudSchedule.length === 0) {
		el.textContent = '';
		return;
	}
	let text = '';
	let grain = sudGrainMass, water = sudWaterMass;
	if (sudStepDescr) {
		// +1: sudStepIndex is the 0-based index Sud.index uses, but the
		// Progress tab's step-plates (createStepPlate()) label 1-based -
		// match it here, same fix as client/brewpi_gui.py's
		// update_status_step_label().
		text = `Step ${sudStepIndex + 1}: ${sudStepDescr}`;
		if (sudStepType === 'hold') {
			const remaining = Math.max(sudHoldRemaining, 0);
			text += ` (${Math.floor(remaining / 60)}:${String(Math.floor(remaining % 60)).padStart(2, '0')} remaining)`;
		}
		if (sudStepIndex !== null && sudStepIndex >= 0 && sudStepIndex < sudSchedule.length) {
			const step = sudSchedule[sudStepIndex];
			grain = step.grain_mass || 0;
			water = step.water_mass || 0;
		}
	}
	const pot = sudPotMass;
	const massText = `Pot: ${pot.toFixed(2)} kg, Water: ${water.toFixed(2)} kg, Grain ${grain.toFixed(2)} kg, total ${(pot + water + grain).toFixed(2)} kg`;
	text = text ? `${text}   ${massText}` : massText;
	const elapsedText = `Elapsed ${elapsed !== null ? formatDuration(elapsed) : '--:--'}`;
	const totalEnergyKwh = (Object.values(energyByStep).reduce((a, b) => a + b, 0) + energyCurrent) / 1000.0;
	const energyText = `Energy ${totalEnergyKwh.toFixed(2)} kWh`;
	el.textContent = `${text}   ${elapsedText}   ${energyText}`;
}

// Mirrors client/brewpi_gui.py's update_sud_actions(): Start also doubles
// as Resume while paused; Stop can still abort a paused run.
function updateSudActions() {
	const canRun = connected && !sudEmpty;
	const running = canRun && RUNNING_STATES.has(sudState);
	const paused = canRun && sudState === PAUSED_STATE;
	document.getElementById('btn-sud-start').disabled = !(canRun && !running);
	document.getElementById('btn-sud-pause').disabled = !running;
	document.getElementById('btn-sud-stop').disabled = !(running || paused);
}

// Mirrors client/brewpi_gui.py's show_user_message(): the Sud is already
// blocked in WAIT_USER server-side - dismissing this dialog (OK or Escape,
// both fire the dialog's 'close' event) is what unblocks it.
function showUserMessage(message) {
	document.getElementById('sud-message-text').textContent = message;
	document.getElementById('sud-message-dialog').showModal();
}

function downloadJson(doc) {
	const blob = new Blob([JSON.stringify(doc, null, '\t')], {type: 'application/json'});
	const url = URL.createObjectURL(blob);
	const a = document.createElement('a');
	a.href = url;
	a.download = `${doc.Name || 'sud'}.json`;
	a.click();
	URL.revokeObjectURL(url);
}

function updateEnvStatusLine() {
	const el = document.getElementById('env-status-line');
	const ambient = ambientTemp !== null ? `${ambientTemp.toFixed(1)}°C` : '-';
	el.textContent = `Ambient: ${ambient}   Warp: ${warpFactor.toFixed(1)}x`;
}

function updateSudForecast(doc) {
	const parsed = parseSudDoc(doc);
	// A genuinely different schedule resets run-progress state; a
	// standalone re-fetch of the *same* running one (e.g. the Save sent
	// on every connect) must not - see client/brewpi_gui.py's
	// update_sud_forecast() for the bug this avoids.
	const isNewSchedule = JSON.stringify(doc) !== JSON.stringify(sudLastLoadedDoc);
	sudLastLoadedDoc = doc;
	sudName = parsed.name;
	sudSchedule = parsed.schedule;
	sudPotMass = parsed.potMass;
	sudGrainMass = parsed.grainMass;
	sudWaterMass = parsed.waterMass;
	sudVolumen = parsed.volumen;
	sudEmpty = sudSchedule.length === 0;
	if (isNewSchedule) {
		sudStepDescr = null;
		sudStepIndex = null;
		sudStepType = null;
		sudHoldRemaining = 0;
		forecastStepStarts = {};
		forecastFinished = false;
		forecastFinalT = null;
		stepActualFrozen = {};
		energyByStep = {};
		energyCurrent = 0;
		elapsed = null;
	}
	rebuildStepPlates();
	updateStatusLine();
	updateSudActions();
	updatePotVisualization();
	document.title = sudName ? `BrewPi - ${sudName}` : 'BrewPi';
}

// --- Channel handlers - mirror client/brewpi_gui.py's on_*_changed(). ---

function onHeaterChanged(msg) {
	for (const key of Object.keys(msg)) {
		if (key === 'Activate') {
			if (initialHeaterActivateSync) {
				document.getElementById('heater-activate').checked = !!msg.Activate;
				initialHeaterActivateSync = false;
			}
		} else if (key === 'PowerSet') {
			document.getElementById('lcd-power-soll').textContent = msg.PowerSet;
		} else if (key === 'Power') {
			document.getElementById('lcd-power-ist').textContent = msg.Power;
			if (initialPowerSync) {
				document.getElementById('heater-power').value = msg.Power;
				document.getElementById('heater-power-readout').textContent = msg.Power;
				initialPowerSync = false;
			}
		} else if (key === 'Capabilities') {
			const power = msg.Capabilities.Power;
			const slider = document.getElementById('heater-power');
			slider.min = power.Min;
			slider.max = power.Max;
		}
	}
}

function onStirrerChanged(msg) {
	for (const key of Object.keys(msg)) {
		if (key === 'Activate') {
			if (initialStirrerActivateSync) {
				document.getElementById('stirrer-activate').checked = !!msg.Activate;
				initialStirrerActivateSync = false;
			}
		} else if (key === 'Speed') {
			stirrerSpeedIst = msg.Speed;
			if (initialSpeedSync) {
				document.getElementById('stirrer-speed').value = msg.Speed;
				document.getElementById('stirrer-speed-readout').textContent = msg.Speed;
				initialSpeedSync = false;
			}
			updateStepPlates();
		} else if (key === 'Capabilities') {
			const power = msg.Capabilities.Power;
			const slider = document.getElementById('stirrer-speed');
			slider.min = power.Min;
			slider.max = power.Max;
		}
	}
}

function onTempCtrlChanged(msg) {
	for (const key of Object.keys(msg)) {
		if (key === 'Soll') {
			const soll = msg.Soll;
			if ('Temp' in soll) {
				document.getElementById('lcd-temp-soll').textContent = soll.Temp.toFixed(1);
				if (initialTempSollSync) {
					document.getElementById('temp-soll').value = Math.round(soll.Temp);
					initialTempSollSync = false;
				}
				document.getElementById('temp-soll-readout').textContent = document.getElementById('temp-soll').value;
			}
			if ('Rate' in soll) {
				const rate = soll.Rate;
				if ('Current' in rate) {
					document.getElementById('lcd-rate-soll').textContent = rate.Current.toFixed(1);
				}
				if ('Set' in rate && initialHeatrateSync) {
					document.getElementById('heatrate-soll').value = rate.Set;
					initialHeatrateSync = false;
				}
			}
		} else if (key === 'Enabled') {
			const enabled = msg.Enabled;
			document.getElementById('tc-enabled').checked = enabled;
			document.getElementById('temp-soll').disabled = !enabled;
			document.getElementById('heatrate-soll').disabled = !enabled;
		}
		if (key === 'Ist') {
			const ist = msg.Ist;
			if ('Temp' in ist) {
				plotTempIst = ist.Temp;
				document.getElementById('lcd-temp-ist').textContent = ist.Temp.toFixed(1);
				updateStepPlates();
				updatePotVisualization();
			}
			if ('Rate' in ist) {
				document.getElementById('lcd-rate-ist').textContent = ist.Rate.toFixed(1);
			}
		}
	}
}

function onSudChanged(msg) {
	for (const key of Object.keys(msg)) {
		if (key === 'Json') {
			// A standalone reply to a just-sent {Save: true} (see
			// btn-sud-save's handler below) - not a schedule update to
			// render, save it to disk instead.
			if (sudSavePending) {
				sudSavePending = false;
				downloadJson(msg.Json);
			} else {
				updateSudForecast(msg.Json);
			}
		} else if (key === 'State') {
			const newState = msg.State;
			const prevState = sudState;
			const isRunningNow = RUNNING_STATES.has(newState);
			if (isRunningNow && !wasRunning) {
				elapsed = 0;
				energyByStep = {};
				energyCurrent = 0;
			}
			wasRunning = isRunningNow;
			sudState = newState;
			if (newState === WAIT_USER_STATE && prevState !== WAIT_USER_STATE && sudUserMessage) {
				showUserMessage(sudUserMessage);
			} else if (prevState === WAIT_USER_STATE && newState !== WAIT_USER_STATE) {
				const dlg = document.getElementById('sud-message-dialog');
				if (dlg.open) dlg.close();
			}
			updateStepPlates();
			updateSudActions();
		} else if (key === 'UserMessage') {
			sudUserMessage = msg.UserMessage;
		} else if (key === 'Step') {
			const step = msg.Step;
			const newIndex = step.Index;
			if (sudStepIndex !== null && newIndex !== null && newIndex > sudStepIndex) {
				for (let i = sudStepIndex; i < newIndex; i++) {
					stepActualFrozen[i] = plotTempIst;
				}
			}
			sudStepIndex = newIndex;
			sudStepDescr = step.Descr;
			sudStepType = step.Type;
			updateStatusLine();
			updateStepPlates();
			updatePotVisualization();
		} else if (key === 'HoldRemaining') {
			sudHoldRemaining = msg.HoldRemaining;
			updateStatusLine();
		} else if (key === 'Elapsed') {
			elapsed = msg.Elapsed;
			updateStepPlates();
			updateStatusLine();
		} else if (key === 'Forecast') {
			const forecast = msg.Forecast;
			forecastStepStarts = Object.fromEntries(forecast.StepStarts);
			forecastFinished = forecast.Finished;
			forecastFinalT = forecast.T.length ? forecast.T[forecast.T.length - 1] : null;
			updateStepPlates();
		} else if (key === 'Energy') {
			energyByStep = Object.fromEntries(msg.Energy.StepEnergy);
			energyCurrent = msg.Energy.Current;
			updateStepPlates();
			updateStatusLine();
		} else if (key === 'Error') {
			if (msg.Error) {
				alert(msg.Error);
			}
		}
	}
}

function onSystemChanged(msg) {
	if ('AmbientTemp' in msg) {
		ambientTemp = msg.AmbientTemp;
		if (document.activeElement !== document.getElementById('ambient-temp')) {
			document.getElementById('ambient-temp').value = msg.AmbientTemp;
		}
		updateEnvStatusLine();
	}
	if ('WarpFactor' in msg) {
		warpFactor = msg.WarpFactor;
		updateEnvStatusLine();
	}
	if ('PlantSim' in msg) {
		document.getElementById('pot-reset-row').classList.toggle('hidden', !msg.PlantSim);
	}
}

const HANDLERS = {
	Pot: () => {},
	Sensor: () => {},
	Heater: onHeaterChanged,
	Stirrer: onStirrerChanged,
	TempCtrl: onTempCtrlChanged,
	Sud: onSudChanged,
	System: onSystemChanged,
};

// --- Connection management. ---

function setConnected(isConnected) {
	connected = isConnected;
	document.getElementById('btn-connect').textContent = isConnected ? 'Disconnect' : 'Connect';
	const statusEl = document.getElementById('conn-status');
	statusEl.textContent = isConnected ? 'Connected' : 'Disconnected';
	statusEl.className = isConnected ? 'status-connected' : 'status-disconnected';
	updateSudActions();
}

function connect() {
	const host = document.getElementById('host').value;
	ws = new WebSocket(host);
	ws.onopen = () => {
		setConnected(true);
		for (const channel of CHANNELS) {
			ws.send(JSON.stringify({'+': channel}));
		}
		// Fetches whatever schedule is already loaded server-side, exactly
		// like client/brewpi_gui.py's Window.connect() - the resulting
		// Json/Forecast pair is handled the same way a real Load is
		// (see updateSudForecast()'s isNewSchedule check).
		sendMsg('Sud', {Save: true});
	};
	ws.onclose = () => setConnected(false);
	ws.onmessage = (event) => {
		const msg = JSON.parse(event.data);
		for (const channel of Object.keys(msg)) {
			const handler = HANDLERS[channel];
			if (handler) {
				handler(msg[channel]);
			}
		}
	};
}

function disconnect() {
	if (ws) {
		ws.close();
	}
}

// --- DOM wiring. ---

document.getElementById('host').value = `ws://${location.hostname}:8765`;

document.getElementById('btn-connect').addEventListener('click', () => {
	if (connected) {
		disconnect();
	} else {
		connect();
	}
});

document.getElementById('heater-power').addEventListener('input', (e) => {
	document.getElementById('heater-power-readout').textContent = e.target.value;
	sendMsg('Heater', {Power: Number(e.target.value)});
});
document.getElementById('heater-activate').addEventListener('change', (e) => {
	sendMsg('Heater', {Activate: e.target.checked ? 1 : 0});
});
document.getElementById('stirrer-speed').addEventListener('input', (e) => {
	document.getElementById('stirrer-speed-readout').textContent = e.target.value;
	sendMsg('Stirrer', {Speed: Number(e.target.value)});
});
document.getElementById('stirrer-activate').addEventListener('change', (e) => {
	sendMsg('Stirrer', {Activate: e.target.checked ? 1 : 0});
});
document.getElementById('temp-soll').addEventListener('input', (e) => {
	document.getElementById('temp-soll-readout').textContent = e.target.value;
	sendMsg('TempCtrl', {Soll: {Temp: Number(e.target.value)}});
});
document.getElementById('heatrate-soll').addEventListener('change', (e) => {
	sendMsg('TempCtrl', {Soll: {Rate: Number(e.target.value)}});
});
document.getElementById('tc-enabled').addEventListener('change', (e) => {
	sendMsg('TempCtrl', {Enable: e.target.checked});
});
document.getElementById('ambient-temp').addEventListener('change', (e) => {
	sendMsg('System', {AmbientTemp: Number(e.target.value)});
});
document.getElementById('btn-pot-reset').addEventListener('click', () => {
	sendMsg('Pot', {Reset: Number(document.getElementById('pot-temp').value)});
});

// New is the same hardcoded empty-doc Load the desktop GUI's
// on_action_sud_new() sends - the server refuses it (like any Load) while a
// run is in progress, surfacing an Error rather than guessing that here.
document.getElementById('btn-sud-new').addEventListener('click', () => {
	sendMsg('Sud', {Load: {Name: '', Description: '', pot_mass: 0, pot_material: null, steps: []}});
});
document.getElementById('btn-sud-load').addEventListener('click', () => {
	document.getElementById('sud-file-input').click();
});
document.getElementById('sud-file-input').addEventListener('change', (e) => {
	const file = e.target.files[0];
	e.target.value = '';
	if (!file) {
		return;
	}
	const reader = new FileReader();
	reader.onload = () => {
		try {
			sendMsg('Sud', {Load: JSON.parse(reader.result)});
		} catch (err) {
			alert(`Not a valid Sud file: ${err.message}`);
		}
	};
	reader.readAsText(file);
});
document.getElementById('btn-sud-save').addEventListener('click', () => {
	sudSavePending = true;
	sendMsg('Sud', {Save: true});
});
document.getElementById('btn-sud-start').addEventListener('click', () => {
	sendMsg('Sud', {Start: true});
});
document.getElementById('btn-sud-pause').addEventListener('click', () => {
	sendMsg('Sud', {Pause: true});
});
document.getElementById('btn-sud-stop').addEventListener('click', () => {
	sendMsg('Sud', {Stop: true});
});
document.getElementById('sud-message-dialog').addEventListener('cancel', e => e.preventDefault());
document.getElementById('sud-message-ok').addEventListener('click', () => {
	sendMsg('Sud', {Confirm: true});
});

document.querySelectorAll('.tab-btn').forEach((btn) => {
	btn.addEventListener('click', () => {
		document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'));
		document.querySelectorAll('.tab-content').forEach((c) => c.classList.remove('active'));
		btn.classList.add('active');
		document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
	});
});

setInterval(() => {
	blinkOn = !blinkOn;
	stepPlates.forEach(applyLedColor);
}, 500);

updateSudActions();
