'use strict';
/**
 * BrightSign device abstraction — wraps the `@brightsign/*` Node modules behind a small, testable API.
 *
 * On a real player these modules are require()-able because BrightSign runs Node and Chromium in ONE
 * shared V8 context (verified: docs.brightsign.biz/developers/nodejs). Off-device (dev laptop) the modules
 * are absent, so every accessor is wrapped in safeRequire()/try and degrades to a no-op/null — the shell
 * still boots and the player still runs, you just don't get hardware control. This is the BrightSign analogue
 * of the desktop telemetry.js "best-effort" model.
 *
 * Module names are the REAL ones (audit-corrected): @brightsign/system, /deviceinfo (serialNumber,
 * getTemperature), /filesysteminfo (NOT storageinfo), /networkconfiguration, /screenshot (JPEG/BMP — we
 * transcode to PNG elsewhere), /videooutput (setPowerSaveMode), /videomodeconfiguration (transform),
 * /cec, /audiooutput (volumeLinear/volumeDb), /registry. There is NO @brightsign/httpserver / roHttpServer.
 */

function safeRequire(name) {
	try {
		return require(name);
	} catch (e) {
		return null;
	}
}

const onDevice = !!safeRequire('@brightsign/system');

/* ───────────────── registry (durable key/value, survives reboot, NOT on the removable cache origin) ───────────────── */

function registrySection(section) {
	const Registry = safeRequire('@brightsign/registry');
	if (!Registry) return null;
	try {
		// @brightsign/registry: new Registry() then .read/.write/.delete(section, key)
		return new Registry();
	} catch (e) {
		return null;
	}
}

function registryRead(section, key) {
	const r = registrySection(section);
	if (!r) return null;
	try {
		const v = r.read(section, key);
		return typeof v === 'string' && v.length ? v : null;
	} catch (e) {
		return null;
	}
}

function registryWrite(section, key, value) {
	const r = registrySection(section);
	if (!r) return false;
	try {
		r.write(section, key, String(value));
		return true;
	} catch (e) {
		return false;
	}
}

function registryDelete(section, key) {
	const r = registrySection(section);
	if (!r) return false;
	try {
		r.delete(section, key);
		return true;
	} catch (e) {
		return false;
	}
}

/* ───────────────── device info (model / serial / OS / temperature) ───────────────── */

function deviceInfo() {
	const DeviceInfo = safeRequire('@brightsign/deviceinfo');
	if (!DeviceInfo) return null;
	try {
		return new DeviceInfo();
	} catch (e) {
		return null;
	}
}

/** Stable hardware id = the unit serial number (used for plane hardware-anchored recovery). */
function serialNumber() {
	const di = deviceInfo();
	if (!di) return null;
	try {
		return di.serialNumber || (di.getSerialNumber && di.getSerialNumber()) || null;
	} catch (e) {
		return null;
	}
}

async function deviceTemperatureC() {
	const di = deviceInfo();
	if (!di || !di.getTemperature) return null;
	try {
		const t = await Promise.resolve(di.getTemperature());
		// returns an object on some OS versions; accept a number or {celsius}
		if (typeof t === 'number') return t;
		if (t && typeof t.celsius === 'number') return t.celsius;
		return null;
	} catch (e) {
		return null;
	}
}

function deviceSummary() {
	const di = deviceInfo();
	if (!di) return {};
	const get = (k, fn) => {
		try {
			return di[k] != null ? di[k] : fn && fn();
		} catch (e) {
			return null;
		}
	};
	return {
		model: get('model'),
		family: get('family'),
		serial: serialNumber(),
		osVersion: get('OSVersion') || get('osVersion'),
		bootVersion: get('BOOTVersion') || get('bootVersion'),
	};
}

/* ───────────────── storage (filesysteminfo — NOT "storageinfo") ───────────────── */

async function storageStats(unit) {
	const FsInfo = safeRequire('@brightsign/filesysteminfo');
	if (!FsInfo) return null;
	try {
		const fsi = new FsInfo();
		const s = await Promise.resolve(fsi.getStorageStats ? fsi.getStorageStats(unit || 'SD') : null);
		if (!s) return null;
		const total = Number(s.sizeBytes != null ? s.sizeBytes : s.size);
		const free = Number(s.bytesFree != null ? s.bytesFree : s.free);
		if (!isFinite(total) || total <= 0) return null;
		const GB = 1024 * 1024 * 1024;
		return { totalGb: round(total / GB), freeGb: round(free / GB), usedGb: round((total - free) / GB) };
	} catch (e) {
		return null;
	}
}

/* ───────────────── network ───────────────── */

function networkInterfaces() {
	const NetCfg = safeRequire('@brightsign/networkconfiguration');
	if (!NetCfg) {
		// dev fallback: Node os
		try {
			const os = require('os');
			const out = [];
			const ifaces = os.networkInterfaces() || {};
			for (const name of Object.keys(ifaces)) {
				for (const a of ifaces[name] || []) {
					if (a.family === 'IPv4' && !a.internal) out.push({ name, ip: a.address, mac: a.mac });
				}
			}
			return out;
		} catch (e) {
			return [];
		}
	}
	try {
		const nc = new NetCfg('eth0');
		const cur = nc.getCurrentConfiguration ? nc.getCurrentConfiguration() : {};
		const ip = (cur && (cur.ip4_address || (cur.ip && cur.ip.address))) || null;
		const mac = (nc.getEthernetMacAddress && nc.getEthernetMacAddress()) || null;
		return ip ? [{ name: 'eth0', ip, mac }] : [];
	} catch (e) {
		return [];
	}
}

/* ───────────────── power / display (CEC + videooutput; charter §5) ───────────────── */

/**
 * Display ON/OFF. BrightSign is the media player, not the panel — so we drive the attached display two ways:
 *  (a) blank/unblank our own HDMI output via videooutput.setPowerSaveMode, and
 *  (b) send a CEC power frame to the TV (4036 = Standby OFF, 400D = "Text View On" wake — audit S-BS-03:
 *      400D is Text View On, a wake frame, NOT "active source").
 * Both are best-effort; on a panel without CEC, only the output-blank applies.
 */
function setDisplayPower(on) {
	let acted = false;
	const VideoOutput = safeRequire('@brightsign/videooutput');
	if (VideoOutput) {
		try {
			const vo = new VideoOutput('HDMI');
			if (vo.setPowerSaveMode) {
				vo.setPowerSaveMode(!on); // power-save = blanked
				acted = true;
			}
		} catch (e) {
			/* ignore */
		}
	}
	const Cec = safeRequire('@brightsign/cec');
	if (Cec) {
		try {
			const cec = new Cec('hdmi 1');
			if (cec.send) {
				cec.send(on ? '400D' : '4036'); // wake (Text View On) / standby — ASCII-hex CEC frames
				acted = true;
			}
		} catch (e) {
			/* ignore */
		}
	}
	return acted;
}

/** Switch the attached display's input source via CEC active-source (opcode 0x82). Best-effort. */
function setDisplayInput(code) {
	const Cec = safeRequire('@brightsign/cec');
	if (!Cec) return false;
	try {
		const cec = new Cec('hdmi 1');
		// active source: 4F:82:<phys-addr-hi>:<phys-addr-lo> — caller passes the desired frame in `code`
		if (cec.send && code) {
			cec.send(String(code));
			return true;
		}
	} catch (e) {
		/* ignore */
	}
	return false;
}

/** Player audio output volume, 0..100 → volumeLinear 0..1 (audit S-BS-07: no setVolume(); use volumeLinear). */
function setVolume(value) {
	const AudioOutput = safeRequire('@brightsign/audiooutput');
	if (!AudioOutput) return false;
	try {
		const ao = new AudioOutput('analog'); // also 'hdmi' / 'usb' depending on routing
		const v = Math.max(0, Math.min(100, Number(value) || 0)) / 100;
		if ('volumeLinear' in ao) {
			ao.volumeLinear = v;
			return true;
		}
	} catch (e) {
		/* ignore */
	}
	return false;
}

/** Panel backlight is display-dependent — only reachable via CEC/RS-232 to a panel that supports it. */
function setBacklight(value) {
	// BrightSign players have no panel backlight of their own; route to the display if a serial protocol is
	// configured. Left as a best-effort CEC user-control passthrough; ack/no-op where unsupported.
	return false;
}

/* ───────────────── orientation (videomodeconfiguration.transform — NOT "videomode") ───────────────── */

function setOrientation(value) {
	const VMC = safeRequire('@brightsign/videomodeconfiguration');
	if (!VMC) return false;
	const map = { portrait: '90', 'reverse-portrait': '270', landscape: 'normal', 'reverse-landscape': '180', normal: 'normal' };
	const transform = map[String(value)] || 'normal';
	try {
		const vmc = new VMC();
		if (vmc.setMode) {
			// setMode preserves the current resolution; only the transform changes
			const cur = (vmc.getActiveMode && vmc.getActiveMode()) || {};
			vmc.setMode(Object.assign({}, cur, { transform }));
			return true;
		}
	} catch (e) {
		/* ignore */
	}
	return false;
}

/* ───────────────── reboot ───────────────── */

function reboot() {
	const System = safeRequire('@brightsign/system');
	if (System) {
		try {
			const s = new System();
			if (s.reboot) {
				s.reboot();
				return true;
			}
		} catch (e) {
			/* ignore */
		}
		try {
			if (System.reboot) {
				System.reboot();
				return true;
			}
		} catch (e) {
			/* ignore */
		}
	}
	return false;
}

/* ───────────────── screenshot (JPEG/BMP native → caller transcodes to PNG) ───────────────── */

/**
 * Capture the framebuffer to a file. @brightsign/screenshot outputs JPEG (default) or BMP — NOT PNG
 * (audit S-BS-02). The plane's /device/screenshot endpoint wants PNG, so agent.js transcodes the captured
 * JPEG to PNG via an offscreen canvas in the page before upload. Returns the path or null.
 */
function captureScreenshot(destPath) {
	const Screenshot = safeRequire('@brightsign/screenshot');
	if (!Screenshot) return null;
	try {
		const ss = new Screenshot();
		// fileType jpeg, full resolution; returns/writes to destPath
		ss.capture ? ss.capture({ filename: destPath, fileType: 'jpeg' }) : ss.Capture && ss.Capture(destPath);
		return destPath;
	} catch (e) {
		return null;
	}
}

/* ───────────────── time zone (roSystemTime; for the boot-time provisioning check) ───────────────── */

/** The device's provisioned timezone (e.g. "Europe/Rome"), or null if unavailable. */
function getTimeZone() {
	const SystemTime = safeRequire('@brightsign/legacy/rosystemtime') || safeRequire('@brightsign/systemtime');
	if (SystemTime) {
		try {
			const st = new SystemTime();
			const tz = st.GetTimeZone ? st.GetTimeZone() : st.timeZone;
			if (tz) return String(tz);
		} catch (e) {
			/* ignore */
		}
	}
	try {
		return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
	} catch (e) {
		return null;
	}
}

/* ───────────────── displays (videooutput EDID per HDMI port; A-8 multi-HDMI telemetry) ───────────────── */

/**
 * Enumerate the attached video outputs. Most BrightSign SKUs are single-HDMI; the XT/XD multi-output models
 * expose HDMI + others. Returns [{index,id,primary,bounds:{width,height},connected}], degrading to [] when
 * @brightsign/videooutput is unavailable (off-device). EDID/mode read is best-effort per port.
 */
function displays() {
	const VideoOutput = safeRequire('@brightsign/videooutput');
	if (!VideoOutput) return [];
	const ports = ['HDMI', 'HDMI2', 'HDMI3', 'HDMI4'];
	const out = [];
	ports.forEach((id, index) => {
		try {
			const vo = new VideoOutput(id);
			// getDisplayMode / getConnectionState vary by OS; read defensively.
			const mode = (vo.getDisplayMode && vo.getDisplayMode()) || (vo.getResolution && vo.getResolution()) || null;
			const connected = vo.getConnectionState ? !!vo.getConnectionState() : index === 0;
			if (!mode && !connected && index > 0) return; // skip phantom ports
			const w = mode && (mode.width || mode.X || mode.x);
			const h = mode && (mode.height || mode.Y || mode.y);
			out.push({
				index,
				id,
				primary: index === 0,
				connected,
				bounds: w && h ? { width: Number(w), height: Number(h) } : null,
			});
		} catch (e) {
			/* port not present */
		}
	});
	return out;
}

/* ───────────────── helpers ───────────────── */

function round(n, d = 1) {
	return typeof n === 'number' && isFinite(n) ? Number(n.toFixed(d)) : null;
}

module.exports = {
	onDevice,
	registryRead,
	registryWrite,
	registryDelete,
	serialNumber,
	deviceSummary,
	deviceTemperatureC,
	storageStats,
	networkInterfaces,
	setDisplayPower,
	setDisplayInput,
	setVolume,
	setBacklight,
	setOrientation,
	reboot,
	captureScreenshot,
	getTimeZone,
	displays,
};
