import { Parser } from "binary-parser";
import * as crc32 from "crc-32";
import { z } from "zod";
import { Roborock } from "../main";
import { cryptoEngine } from "./cryptoEngine";

export type ProtocolVersion = "1.0" | "A01" | "L01" | "B01" | "\x81S\x19";

const SUPPORTED_VERSIONS: ProtocolVersion[] = ["1.0", "A01", "L01", "B01", "\x81S\x19"] as const;

// Zod schema for runtime frame validation
const FrameSchema = z.object({
	version: z.string(),
	seq: z.number(),
	random: z.number(),
	timestamp: z.number(),
	protocol: z.number(),
	payloadLen: z.number(),
	payload: z.instanceof(Buffer),
	crc32: z.number(),
});

// Infer Frame type from schema
export type Frame = z.infer<typeof FrameSchema> & { version: ProtocolVersion };

// --------------------
// Constants
// --------------------

const HEADER_LEN = 3 + 4 + 4 + 4 + 2 + 2; // version(3) + seq(4) + random(4) + timestamp(4) + protocol(2) + payloadLen(2)
const CRC32_LEN = 4;
const MAX_SOCKET_MESSAGE_ID = 0xffff;

function nextSocketRandom(): number {
	return Math.floor(Math.random() * 1_000_000 + 1_000) >>> 0;
}

// --------------------
// Binary Parser Configuration
// --------------------

const frameParser = new Parser()
	.endianess("big")
	.string("version", { length: 3 })
	.uint32("seq")
	.uint32("random")
	.uint32("timestamp")
	.uint16("protocol")
	.uint16("payloadLen")
	.buffer("payload", { length: "payloadLen" })
	.uint32("crc32");

// --------------------
// CRC Utilities
// --------------------

/**
 * Validates CRC32 checksum.
 */
function validateCrc(buf: Buffer): boolean {
	const crc = crc32.buf(buf.subarray(0, buf.length - 4)) >>> 0;
	return crc === buf.readUInt32BE(buf.length - 4);
}

/**
 * Appends CRC32 checksum to the buffer.
 */
function appendCrc(buf: Buffer): void {
	const crc = crc32.buf(buf.subarray(0, buf.length - 4)) >>> 0;
	buf.writeUInt32BE(crc, buf.length - 4);
}

// --------------------
// Protocol Version Dispatchers
// --------------------

const decryptors: Record<ProtocolVersion, (...args: any[]) => Buffer> = {
	"1.0": (payload, key, timestamp) => cryptoEngine.decryptV1(payload, key, timestamp),
	A01: (payload, key, random) => cryptoEngine.decryptA01(payload, key, random),
	L01: (payload, key, timestamp, seq, random, connectNonce, ackNonce) => cryptoEngine.decryptL01(payload, key, timestamp, seq, random, connectNonce, ackNonce),
	B01: (payload, key, random) => cryptoEngine.decryptB01(payload, key, random),
	"\x81S\x19": (payload, key, random) => cryptoEngine.decryptB01(payload, key, random),
};

const encryptors: Record<ProtocolVersion, (...args: any[]) => Buffer> = {
	"1.0": (payload, key, timestamp) => cryptoEngine.encryptV1(payload, key, timestamp),
	A01: (payload, key, random) => cryptoEngine.encryptA01(payload, key, random),
	L01: (payload, key, timestamp, seq, random, connectNonce, ackNonce) => cryptoEngine.encryptL01(payload, key, timestamp, seq, random, connectNonce, ackNonce),
	B01: (payload, key, random) => cryptoEngine.encryptB01(payload, key, random),
	"\x81S\x19": (payload, key, random) => cryptoEngine.encryptB01(payload, key, random),
};

export class messageParser {
	adapter: Roborock;
	private transportSequences = new Map<string, number>();

	constructor(adapter: Roborock) {
		this.adapter = adapter;
	}

	resetTransportSequence(duid: string, nextSequenceId = 1): void {
		const normalized = nextSequenceId >>> 0;
		this.transportSequences.set(duid, normalized === 0 || normalized > MAX_SOCKET_MESSAGE_ID ? 1 : normalized);
	}

	nextTransportSequenceId(duid: string): number {
		const stored = this.transportSequences.get(duid) ?? 1;
		const current = stored === 0 || stored > MAX_SOCKET_MESSAGE_ID ? 1 : stored;
		const next = current >= MAX_SOCKET_MESSAGE_ID ? 1 : current + 1;
		this.transportSequences.set(duid, next);
		return current;
	}

	/**
	 * Decodes a buffer containing Roborock protocol messages.
	 * Returns an array of frames (empty if none decoded).
	 */
	decodeMsg(message: Buffer, duid: string): Frame[] {
		const decoded: Frame[] = [];
		let offset = 0;

		while (offset + 3 <= message.length) {
			// Check protocol version
			const version = message.toString("latin1", offset, offset + 3) as ProtocolVersion;

			if (!SUPPORTED_VERSIONS.includes(version)) {
				this.adapter.rLog("Requests", duid, "Error", version, undefined, `Unsupported version at offset ${offset} | Hex: ${message.toString("hex")}`, "error");

				// Skip corrupted message block
				const MIN_MSG_LENGTH = 23;
				offset += MIN_MSG_LENGTH;
				continue;
			}

			let raw: unknown;
			try {
				raw = frameParser.parse(message.subarray(offset));
			} catch (err) {
				this.adapter.rLog("Requests", duid, "Error", version, undefined, `Parse failed at offset ${offset}: ${err}`, "error");
				break;
			}

			let data: Frame;
			try {
				data = FrameSchema.parse(raw) as Frame;
				data.version = version;
			} catch (err) {
				this.adapter.rLog("Requests", duid, "Error", version, undefined, `Validation failed: ${err}`, "error");
				break;
			}

			const msgLen = HEADER_LEN + data.payloadLen + CRC32_LEN;
			if (msgLen <= 0 || offset + msgLen > message.length) break;

			// Validate CRC
			const msgBuffer = message.subarray(offset, offset + msgLen);
			if (!validateCrc(msgBuffer)) {
				this.adapter.rLog("Requests", duid, "Error", version, undefined, `CRC32 mismatch at offset ${offset}`, "error");
				offset += msgLen;
				continue;
			}

			// Get local key
			const localKey = this.adapter.http_api.getMatchedLocalKeys().get(duid);
			if (!localKey) {
				this.adapter.rLog("Requests", duid, "Error", version, undefined, "No localKey found", "error");
				offset += msgLen;
				continue;
			}

			// For B01 protocol 300/301/302: keep raw payload and full frame for chunk assembler / record_map
			if (version === "B01" && (data.protocol === 300 || data.protocol === 301 || data.protocol === 302)) {
				(data as any).rawPayload = Buffer.from(data.payload);
				(data as any).rawFrame = Buffer.from(msgBuffer);
			}
			// Decrypt
			try {
				if (version === "L01") {
					const dev = this.adapter.local_api.localDevices[duid];
					if (!dev?.connectNonce || dev.ackNonce == null) {
						throw new Error(`Missing nonces for L01 (duid=${duid})`);
					}
					data.payload = decryptors.L01(data.payload, localKey, data.timestamp, data.seq, data.random, dev.connectNonce, dev.ackNonce);
				} else if (version === "1.0") {
					data.payload = decryptors["1.0"](data.payload, localKey, data.timestamp);
				} else if (version === "A01") {
					data.payload = decryptors.A01(data.payload, localKey, data.random);
				} else if (version === "B01") {
					data.payload = decryptors.B01(data.payload, localKey, data.random);
				}
				decoded.push(data);
			} catch (err: unknown) {
				this.adapter.rLog("Requests", duid, "Error", version, undefined, `Decryption failed at offset ${offset}: ${this.adapter.errorMessage(err)} | Hex: ${message.toString("hex")}`, "error");
			}

			offset += msgLen;
		}

		return decoded;
	}

	/**
	 * Builds JSON payload for device command.
	 */
	async buildPayload(protocol: number, messageID: number, method: string, params: any, version: string): Promise<string> {
		const timestamp = Math.floor(Date.now() / 1000);
		const endpoint = await this.adapter.mqtt_api.ensureEndpoint();

		// Protocol A01 simplified payload
		if (version === "A01") {
			return JSON.stringify({ dps: { [method]: params }, t: timestamp });
		}

		// Standard payload
		const inner: any = { id: messageID, method, params };

		// Add security context ONLY for MQTT
		if (method === "get_photo" && protocol === 101) {
			const kp = cryptoEngine.ensureRsaKeys();
			// converting params to any to avoid TS errors
			const p = params as any;

			// FORCE Endpoint to "xxx" as per S7 MaxV protocol (and user log)
			p.endpoint = "xxx";

			// FORCE Cipher Suite 1 (RSA+AES)
			const cipherSuite = 1;

			// FORCE Security Object to match S7 MaxV (Cipher 1 + RSA, NO NONCE here)
			p.security = {
				cipher_suite: cipherSuite,
				pub_key: {
					e: kp.public.e,
					n: kp.public.n,
				},
			};

			// Add root-level security (nonce/endpoint)
			inner.security = {
				endpoint: endpoint,
				nonce: this.adapter.nonce.toString("hex").toUpperCase(),
			};
		} else if (["get_map_v1", "get_clean_record_map"].includes(method)) {
			// For TCP get_photo (and maps), we need the basic security wrapper (endpoint+nonce)
			// but NOT the RSA keys in params.
			inner.security = {
				endpoint,
				nonce: this.adapter.nonce.toString("hex").toUpperCase(),
			};
		}

		if (version === "B01") {
			inner.msgId = String(messageID);

			if (method === "prop.get" || method === "prop.set" || method === "prop" || method.startsWith("service.")) {
				inner.method = method === "prop" ? "prop.set" : method;
				inner.params = params;

				if (typeof inner.params === "string") {
					try {
						inner.params = JSON.parse(inner.params);
					} catch  {
						// Keep as string if parse fails
					}
				}
				if (typeof inner.params === "object" && inner.params !== null && !Array.isArray(inner.params)) {
					const paramObj = inner.params as Record<string, any>;
					if (paramObj.fan_power !== undefined) {
						paramObj.wind = paramObj.fan_power;
						delete paramObj.fan_power;
					}
					if (paramObj.water_box_mode !== undefined) {
						paramObj.water = paramObj.water_box_mode;
						delete paramObj.water_box_mode;
					}
					if (paramObj.mop_mode !== undefined) {
						paramObj.mode = paramObj.mop_mode;
						delete paramObj.mop_mode;
					}
				}
			} else if (method === "get_prop") {
				inner.method = "prop.get";
				inner.params = { property: params };
			} else if (method === "get_map_v1") {
				inner.method = "service.upload_by_maptype";
				inner.params = { force: 1, map_type: 0 };
			} else if (method === "get_room_mapping") {
				inner.method = "service.get_map_list";
				inner.params = {};
			} else if (["app_start", "app_stop", "app_pause", "app_charge"].includes(method)) {
				// Maps to prop.set { status: X }
				const statusMap: Record<string, number> = {
					app_start: 1,
					app_stop: 2,
					app_pause: 10,
					app_charge: 6
				};
				inner.method = "prop.set";
				inner.params = { status: statusMap[method] };
			} else if (method === "set_custom_mode") {
				// Fan Power
				inner.method = "prop.set";
				inner.params = { wind: params[0] };
			} else if (method === "set_water_box_custom_mode") {
				// Water Level
				inner.method = "prop.set";
				inner.params = { water: params[0] };
			} else if (method === "set_mop_mode") {
				if (params[0] >= 300) {
					inner.method = "prop.set";
					inner.params = { mode: params[0] };
				} else {
					inner.method = "prop.set";
					inner.params = { water: params[0] };
				}
			} else if (["along_floor", "green_laser", "status", "wind", "water", "fan_power", "water_box_mode", "mop_mode"].includes(method)) {
				// Handle both legacy names (fan_power) and B01 native names (wind)
				const keyMap: Record<string, string> = {
					"fan_power": "wind",
					"water_box_mode": "water",
					"mop_mode": "mode" // assuming mop_mode maps to 'mode' or similar
				};
				const key = keyMap[method] || method;
				inner.method = "prop.set";
				inner.params = { [key]: params[0] };
			} else if (method === "app_zoned_clean") {
				inner.method = "service.zoned_clean";
				inner.params = { zones: params[0] };
			}

			// Cloud expects dps.10000 = { method, msgId, params } in this order (no id).
			const b01Inner = { method: inner.method, msgId: String(messageID), params: inner.params };
			return JSON.stringify({ dps: { "10000": b01Inner }, t: timestamp });
		}

		// Local TCP uses SocketFrameType.PUBLISH (4) as the outer frame type, but
		// app RPC payloads are still carried in dps.101.
		const dpsKey = protocol === 4 ? 101 : protocol;
		return JSON.stringify({ dps: { [dpsKey]: JSON.stringify(inner) }, t: timestamp });
	}

	/**
	 * Builds complete Roborock binary frame.
	 */
	async buildRoborockMessage(duid: string, protocol: number, timestamp: number, payload: string | Buffer, version: string, sequenceId?: number): Promise<Buffer | false> {
		const s = (sequenceId !== undefined ? sequenceId : this.nextTransportSequenceId(duid)) >>> 0;
		const r = nextSocketRandom();

		const localKey = this.adapter.http_api.getMatchedLocalKeys().get(duid);
		if (!localKey) return false;

		// Protocol 1 (Handshake)
		if (protocol === 1) {
			const msg = Buffer.alloc(HEADER_LEN + CRC32_LEN);
			msg.write(version);
			msg.writeUInt32BE(s, 3);
			msg.writeUInt32BE(r, 7);
			msg.writeUInt32BE(timestamp >>> 0, 11);
			msg.writeUInt16BE(protocol, 15);
			msg.writeUInt16BE(0, 17); // Payload 0
			appendCrc(msg);
			return msg;
		}

		let encrypted: Buffer;
		const payloadBuf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf-8");

		// Encrypt
		if (version === "L01") {
			const connectNonce = this.adapter.local_api.localDevices[duid]?.connectNonce;
			const ackNonce = this.adapter.local_api.localDevices[duid]?.ackNonce;

			if (!connectNonce || ackNonce == null) return false;

			encrypted = encryptors.L01(payloadBuf, localKey, timestamp, s, r, connectNonce, ackNonce);
		} else if (version === "1.0") {
			encrypted = encryptors["1.0"](payloadBuf, localKey, timestamp);
		} else if (version === "A01") {
			encrypted = encryptors.A01(payloadBuf, localKey, r);
		} else if (version === "B01") {
			encrypted = encryptors.B01(payloadBuf, localKey, r);
		} else {
			return false; // Unsupported
		}

		// Assemble message
		const msg = Buffer.alloc(HEADER_LEN + encrypted.length + CRC32_LEN);

		msg.write(version);
		msg.writeUInt32BE(s, 3);
		msg.writeUInt32BE(r, 7);
		msg.writeUInt32BE(timestamp >>> 0, 11);
		msg.writeUInt16BE(protocol, 15);
		msg.writeUInt16BE(encrypted.length, 17);
		encrypted.copy(msg, 19);

		appendCrc(msg);

		return msg;
	}
}
