import type { AxiosInstance, InternalAxiosRequestConfig } from "axios";
import axios from "axios";
import * as crypto from "node:crypto";
import { Roborock } from "../main";
import { LoginV4Response, ProductV5Response } from "./apiTypes";
import { cryptoEngine } from "./cryptoEngine";

// Constants
const API_V3_SIGN = "api/v3/key/sign";
const API_V4_LOGIN_CODE = "api/v4/auth/email/login/code";
const API_V4_LOGIN_PASSWORD = "api/v4/auth/email/login/pwd";
const API_V4_EMAIL_CODE = "api/v4/email/code/send";
const API_V5_PRODUCT = "api/v5/product";

interface RegionConfig {
	apiBaseUrl: string;
	loginCountry: string;
	loginCountryCode: string;
}

const REGION_CONFIG: Record<string, RegionConfig> = {
	eu: {
		apiBaseUrl: "https://euiot.roborock.com",
		loginCountry: "DE",
		loginCountryCode: "49",
	},
	us: {
		apiBaseUrl: "https://usiot.roborock.com",
		loginCountry: "US",
		loginCountryCode: "1",
	},
	cn: {
		apiBaseUrl: "https://cniot.roborock.com",
		loginCountry: "CN",
		loginCountryCode: "86",
	},
	asia: {
		apiBaseUrl: "https://api.roborock.com",
		loginCountry: "SG", // Default to Singapore for general Asia
		loginCountryCode: "65",
	},
};

// --------------------
// Interfaces & Types
// --------------------

interface RriotData {
	u: string;
	s: string;
	h: string;
	k: string;
	r: { a: string; m: string };
}

interface UserData {
	token: string;
	rriot: RriotData;
}

export interface Device {
	duid: string;
	localKey: string;
	productId: string;
	name?: string;
	featureSet?: number;
	newFeatureSet?: string;
	online: boolean;
	deviceStatus: Record<string, unknown>;
	pv: string;
	sn?: string;
}

interface Product {
	id: string;
	model: string;
	category: string;
	name?: string;
}

interface Room {
	id: number;
	name: string;
}

export interface Scene {
	id: number;
	name: string;
	enabled: boolean;
	param: string; // JSON string
}

export interface SceneResponse {
	result: Scene[];
}

interface HomeData {
	rrHomeId: number;
	products: Product[];
	devices: Device[];
	receivedDevices: Device[];
	rooms: Room[];
}

/**
 * Helper to calculate MD5 hex string
 */
function md5hex(str: string): string {
	return crypto.createHash("md5").update(str).digest("hex");
}

export class http_api {
	adapter: Roborock;
	loginApi: AxiosInstance | null = null;
	realApi: AxiosInstance | null = null;
	userData: UserData | null = null;
	homeData: HomeData | null = null;
	homeID: number | null = null;
	public productInfo: ProductV5Response | null = null;

	private fwFeaturesCache = new Map<string, number[]>();

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

	/**
	 * Initializes the HTTP API and authentication for Roborock Cloud.
	 * @see test/unit/cloud_api_specification.test.ts for the cloud login flow and Hawk signing details.
	 * @param clientID The client identifier.
	 */
	async init(clientID: string): Promise<void> {
		// Initialize the login API (needed to get access to the real API)
		const region = this.adapter.config.region || "eu";
		const regionConfig = REGION_CONFIG[region] || REGION_CONFIG["eu"];

		this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, `Initializing HTTP API with region: ${region} (${regionConfig.apiBaseUrl})`, "info");

		this.loginApi = axios.create({
			baseURL: regionConfig.apiBaseUrl,
			headers: {
				header_clientid: crypto.createHash("md5").update(this.adapter.config.username).update(clientID).digest().toString("base64"),
				header_appversion: "4.57.02",
				header_clientlang: "de",
				header_phonemodel: "Pixel 9 Pro XL",
				header_phonesystem: "Android",
			},
		});

		// Attempt to restore session
		await this.loadUserData();

		await this.initializeRealApi();

		try {
			await this.getHomeID();
		} catch (e: unknown) {
			const msg = this.adapter.errorMessage(e);
			if (msg && (msg.includes("invalid token") || msg.includes("auth_failed"))) {
				this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, "Token expired or invalid. Clearing session and re-authenticating...", "warn");

				// Clear bad data
				this.userData = null;
				this.homeID = null;
				await this.adapter.setState("UserData", { val: "", ack: true });

				// Re-initialize (will force fresh login because userData is null)
				await this.initializeRealApi();
				await this.getHomeID();
			} else {
				throw e; // Rethrow other errors
			}
		}
	}

	/**
	 * Restores UserData from state.
	 */
	async loadUserData(): Promise<void> {
		try {
			const userDataState = await this.adapter.getStateAsync("UserData");
			if (userDataState && userDataState.val) {
				const data = JSON.parse(userDataState.val as string);
				if (data && data.token && data.rriot) {
					this.userData = data;
					this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, "Restored persisted UserData.", "info");
				}
			}
		} catch {
			this.adapter.rLog("HTTP", null, "Debug", "Cloud", undefined, "No previous UserData found or invalid.", "debug");
		}
	}

	/**
	 * Logs in (if necessary) and sets up the authenticated "Real API" with Hawk authentication.
	 */
	private loginCodeResolver: ((code: string) => void) | null = null;

	public submitLoginCode(code: string): void {
		if (this.loginCodeResolver) {
			this.loginCodeResolver(code);
			this.loginCodeResolver = null;
		}
	}

	async initializeRealApi(): Promise<void> {
		this.adapter.rLog("HTTP", null, "Debug", "Cloud", undefined, "Initializing Real API (Hawk Auth)", "debug");

		if (!this.loginApi) {
			throw new Error("loginApi is not initialized. Call init() first.");
		}
		if (!this.userData) {
			try {
				let usePasswordFlow = this.adapter.config.loginMethod === "password" && !!this.adapter.config.password;

				// 1. Sign Request (Get K) - needed for both password and code login (for password encryption or code verification)
				// Use a random 16-char string for 's' (nonce/salt)
				const s = crypto.randomBytes(12).toString("base64").substring(0, 16).replace(/\+/g, "X").replace(/\//g, "Y");
				const signData = await this.signRequest(s);
				if (!signData) throw new Error("Failed to obtain signature key k");
				const k = signData.k;

				if (usePasswordFlow) {
					this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, "Starting Password Login Flow...", "info");
					try {
						const loginResult = await this.loginByPassword(this.adapter.config.password, k, s);
						if (loginResult.code === 200) {
							this.userData = loginResult.data!;
							this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, "Login with password successful.", "info");
						} else if (loginResult.code === 2031) {
							this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, "Password login requires 2FA (Code 2031). Falling back to 2FA flow.", "warn");
							usePasswordFlow = false;
						} else {
							throw new Error(`Login with password failed: ${JSON.stringify(loginResult)}`);
						}
					} catch (e: unknown) {
						this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `Password login error: ${this.adapter.errorMessage(e)}`, "error");
						// If explicit 2031 (handled above) or other error, we might decide to fallback or fail.
						// Current logic: if it was 2031, usePasswordFlow is set to false, so we fall through to 2FA.
						// If it was another error (e.g. wrong password), we should probably stop?
						// For safety/flexibility, if we flagged 'false' above, we continue. If we threw, we stop.
						if (usePasswordFlow) throw e;
					}
				}

				// If not using password flow (or fell back)
				if (!this.userData && !usePasswordFlow) {
					this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, "Starting Direct 2FA Login Flow...", "info");

					// 1. Request Email Code
					await this.requestEmailCode(this.adapter.config.username);

					const warning = [
						"********************************************************************************",
						"ATTENTION: 2FA Code required!",
						`An email has been sent to ${this.adapter.config.username}.`,
						"Please enter the 6-digit code into the state 'roborock.0.loginCode' immediately.",
						"********************************************************************************"
					].join("\n");

					this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, warning, "error");

					// State at root: roborock.0.loginCode
					const stateId = "loginCode";
					await this.adapter.ensureState(stateId, { name: "2FA Login Code", write: true, type: "string", def: "" });
					await this.adapter.setState(stateId, { val: "", ack: true });

					await this.adapter.subscribeStatesAsync(stateId);

					// 2. Wait for Code
					let code = "";
					try {
						code = await new Promise<string>((resolve, reject) => {
							this.loginCodeResolver = resolve;
							// Timeout after 15 minutes
							setTimeout(() => {
								if (this.loginCodeResolver) {
									this.loginCodeResolver = null;
									reject(new Error("Timeout waiting for 2FA code"));
								}
							}, 15 * 60 * 1000); // 15 min
						});
					} catch (e: unknown) {
						throw e;
					}

					this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, "2FA code received. Proceeding with login...", "info");
					await this.adapter.unsubscribeStatesAsync(stateId);

					// 3. Login with Code
					// Regenerate signature to avoid expiration
					const newS = crypto.randomBytes(12).toString("base64").substring(0, 16).replace(/\+/g, "X").replace(/\//g, "Y");
					const newSignData = await this.signRequest(newS);

					if (!newSignData) {
						this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `Failed to re-obtain signature for 2FA.`, "error");
						throw new Error("Failed to re-obtain signature for 2FA login");
					}

					const loginResult = await this.loginWithCode(code, newSignData.k, newS);

					if (loginResult.code === 200) {
						this.userData = loginResult.data!; // data IS UserData
						await this.adapter.setState(stateId, { val: "", ack: true });
					} else {
						throw new Error(`Login with code failed: ${JSON.stringify(loginResult)}`);
					}
				}

				if (!this.userData) {
					throw new Error("Login returned empty userdata.");
				}

				await this.adapter.setState("UserData", { val: JSON.stringify(this.userData), ack: true });

				// Load product definitions (V5 API)
				try {
					this.productInfo = await this.getProductInfoV5();
					if (this.productInfo) {
						let count = 0;
						if (this.productInfo.data && this.productInfo.data.categoryDetailList) {
							for (const cat of this.productInfo.data.categoryDetailList) {
								if (cat.productList) count += cat.productList.length;
							}
						}
						this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, `V5 Product Info fetched. ${count} products available.`, "info");
						await this.adapter.setState("info.productInfo", { val: JSON.stringify(this.productInfo), ack: true });
					}
				} catch (err) {
					this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, `Failed to get product info V5: ${err}`, "warn");
				}
			} catch (error: unknown) {
				this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `Error in initializeRealApi: ${this.adapter.errorMessage(error)}`, "error");
				throw error;
			}
		}

		if (!this.userData?.token) {
			throw new Error("Failed to retrieve user token. Check login credentials.");
		}

		// Set global auth header for loginApi (though mostly unused after this)
		this.loginApi.defaults.headers.common["Authorization"] = this.userData.token;

		try {
			const rriot = this.get_rriot();

			// Initialize the real API with Hawk Authentication Interceptor
			const realApi = axios.create({
				baseURL: this.userData.rriot.r.a,
				headers: {
					"x-iotsdk-version": "1.0.1",
					"x-app-name": "com.roborock.smart",
					"x-app-version-code": "100834",
					"x-app-version-name": "4.57.02",
					"x-uid": this.userData.rriot.u,
					"User-Agent": "UA=RRSDKAndroid/1.0.1",
				},
			});

			realApi.interceptors.request.use((config: InternalAxiosRequestConfig) => {
				const timestamp = Math.floor(Date.now() / 1000);
				const nonce = crypto
					.randomBytes(6)
					.toString("base64")
					.substring(0, 6)
					.replace(/[+/]/g, (m) => (m === "+" ? "X" : "Y"));

				// Calculate signature
				let urlPath = "";
				if (config.url) {
					// Handle relative URLs correctly by creating a dummy base if needed
					// or using the instance's baseURL if config.url is relative
					const fullUrl = axios.getUri(config);
					try {
						// Provide a dummy base to handle relative URLs returned by getUri
						const urlObj = new URL(fullUrl, "http://dummy");
						urlPath = urlObj.pathname + urlObj.search;
					} catch {
						// Fallback if URL construction fails
						urlPath = config.url || "";
					}
				}

				const prestr = [rriot.u, rriot.s, nonce, timestamp, md5hex(urlPath), "", ""].join(":");
				const mac = crypto.createHmac("sha256", rriot.h).update(prestr).digest("base64");

				config.headers["Authorization"] = `Hawk id="${rriot.u}", s="${rriot.s}", ts="${timestamp}", nonce="${nonce}", mac="${mac}"`;

				return config;
			});

			this.realApi = realApi;

			await this.adapter.setState("info.connection", { val: true, ack: true });
		} catch (error: unknown) {
			this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `Error in initializeRealApi: ${this.adapter.errorStack(error)}`, "error");
			await this.adapter.setState("info.connection", { val: false, ack: true });
		}
	}

	async requestEmailCode(username: string): Promise<void> {
		if (!this.loginApi) throw new Error("loginApi is not initialized.");

		try {
			const params = new URLSearchParams();
			params.append("type", "login");
			params.append("email", username);
			params.append("platform", "");

			const res = await this.loginApi.post(API_V4_EMAIL_CODE, params.toString());

			if (res.data && res.data.code != 200) {
				throw new Error(`Start 2FA failed: ${res.data.msg} (Code: ${res.data.code})`);
			}
		} catch (error: unknown) {
			const err = error as { response?: { data?: unknown } };
			if (err?.response?.data !== undefined) {
				this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `Request email code failed with response: ${JSON.stringify(err.response.data)}`, "error");
			}
			throw error; // Re-throw exact error to be caught by caller
		}
	}

	async signRequest(s: string): Promise<{ k: string } | null> {
		if (!this.loginApi) return null;

		try {
			const res = await this.loginApi.post(`${API_V3_SIGN}?s=${s}`);
			return res.data.data;
		} catch (e: unknown) {
			this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `SignRequest failed: ${this.adapter.errorMessage(e)}`, "error");
			return null;
		}
	}

	async loginWithCode(code: string, k: string, s: string): Promise<LoginV4Response> {
		if (!this.loginApi) throw new Error("loginApi not initialized");

		const headers = {
			"x-mercy-k": k,
			"x-mercy-ks": s
			// content-type application/x-www-form-urlencoded is default for axios with URLSearchParams
		};

		const region = this.adapter.config.region || "eu";
		const regionConfig = REGION_CONFIG[region] || REGION_CONFIG["eu"];

		const params = new URLSearchParams({
			country: regionConfig.loginCountry,
			countryCode: regionConfig.loginCountryCode,
			email: this.adapter.config.username,
			code: code,
			majorVersion: "14",
			minorVersion: "0"
		});

		try {
			const res = await this.loginApi.post(API_V4_LOGIN_CODE, params.toString(), { headers });
			return res.data;
		} catch (e: unknown) {
			throw new Error(`Login with code failed: ${this.adapter.errorMessage(e)}`);
		}
	}

	async loginByPassword(password: string, k: string, s: string): Promise<LoginV4Response> {
		if (!this.loginApi) throw new Error("loginApi not initialized");

		this.adapter.rLog("HTTP", null, "->", "Cloud", undefined, `Attempting Password Login for user: ${this.adapter.config.username}`, "info");

		const encryptedPassword = cryptoEngine.encryptPassword(password, k);

		const headers = {
			"x-mercy-k": k,
			"x-mercy-ks": s
		};

		const params = new URLSearchParams({
			email: this.adapter.config.username,
			password: encryptedPassword,
			majorVersion: "14",
			minorVersion: "0"
		});

		try {
			const res = await this.loginApi.post(API_V4_LOGIN_PASSWORD, params.toString(), { headers });
			return res.data;
		} catch (e: unknown) {
			const err = e as { response?: { data?: LoginV4Response } };
			if (err?.response?.data) {
				const errData = err.response.data as LoginV4Response;
				this.adapter.rLog("HTTP", null, "<-", "Cloud", undefined, `Login Failed. Code: ${errData.code}, Msg: ${errData.msg}`, "error");
				return errData;
			}
			throw new Error(`Login with password failed: ${this.adapter.errorMessage(e)}`);
		}
	}

	async getProductInfoV5(): Promise<ProductV5Response | null> {
		if (!this.loginApi) return null;

		try {
			const res = await this.loginApi.get(API_V5_PRODUCT);

			if (res.data && res.data.data) {
				this.productInfo = res.data; // Store it
				return res.data;
			}
			return null;
		} catch (e: unknown) {
			this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, `getProductInfoV5 failed: ${this.adapter.errorMessage(e)}`, "warn");
			return null;
		}
	}

	async ensureProductInfo(): Promise<void> {
		if (!this.productInfo) {
			this.adapter.rLog("HTTP", null, "Debug", "Cloud", undefined, "ProductInfo not present. Fetching V5 Product Info...", "debug");
			await this.getProductInfoV5();
		}
	}

	async downloadProductImages() {
		if (!this.adapter.config.downloadRoborockImages) return;
		await this.ensureProductInfo();

		if (!this.productInfo?.data?.categoryDetailList) {
			this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, "Cannot download images: categoryDetailList missing.", "warn");
			return;
		}

		for (const cat of this.productInfo.data.categoryDetailList) {
			if (!cat.productList) continue;
			for (const p of cat.productList) {
				if (p.picurl) {
					try {
						const safeId = p.model.replace(/\./g, "_");
						const res = await axios.get(p.picurl, { responseType: "arraybuffer" });
						if (res.status === 200) {
							const base64 = Buffer.from(res.data, "binary").toString("base64");
							const stateId = `Products.${safeId}.image`;

							await this.adapter.setObjectNotExistsAsync(stateId, {
								type: "state",
								common: {
									name: p.model + " Image",
									type: "string",
									role: "value",
									read: true,
									write: false
								},
								native: {}
							});
							await this.adapter.setState(stateId, { val: base64, ack: true });
						}
					} catch (e) {
						this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, `Failed to download image for ${p.model}: ${e}`, "warn");
					}
				}
			}
		}
	}

	/**
	 * Retrieves the Home ID from the API.
	 */
	async getHomeID(): Promise<void> {
		if (!this.loginApi) {
			throw new Error("loginApi is not initialized. Call init() first.");
		}

		try {
			const response = await this.loginApi.get("api/v1/getHomeDetail");
			if (response.data.data) {
				this.adapter.rLog("HTTP", null, "Debug", "Cloud", undefined, `getHomeDetail: ${JSON.stringify(response.data)}`, "debug");
				this.homeID = response.data.data.rrHomeId;
				this.adapter.rLog("HTTP", null, "Debug", "Cloud", undefined, `this.homeID: ${this.homeID}`, "debug");
			} else {
				this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `failed to get getHomeDetail: ${response.data.msg}`, "error");

				if (response.data.msg === "invalid token" || response.data.code === 401) {
					throw new Error("invalid token");
				}
			}
		} catch (error: unknown) {
			this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `Error getting HomeID: ${this.adapter.errorMessage(error)}`, "error");
			throw error;
		}
	}

	/**
	 * Downloads the latest Home Data (Devices, Rooms, Products) and stores it in state.
	 * Uses GET v3/user/homes/{homeID} only (same as Roborock app).
	 */
	async updateHomeData(): Promise<void> {
		if (!this.loginApi) throw new Error("loginApi is not initialized. Call init() first.");
		if (!this.realApi) throw new Error("realApi is not initialized. Call initializeRealApi() first");

		if (this.homeID) {
			try {
				const res = await this.realApi.get<{ success?: boolean; result?: { id: number; products?: Product[]; devices?: Device[]; receivedDevices?: Device[]; rooms?: Room[] } }>(`v3/user/homes/${this.homeID}`);

				if (res.data?.success && res.data?.result) {
					const result = res.data.result;
					this.homeData = {
						rrHomeId: result.id,
						products: result.products || [],
						devices: result.devices || [],
						receivedDevices: result.receivedDevices || [],
						rooms: result.rooms || []
					};
					this.adapter.rLog("HTTP", null, "<-", "Cloud", undefined, `HomeData updated (HomeID: ${this.homeID}, Devices: ${this.homeData.devices?.length}, Received: ${this.homeData.receivedDevices?.length})`, "debug");
				} else {
					this.homeData = null;
					this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, "V3 HomeData response missing or not success.", "warn");
				}

				await this.adapter.setState("HomeData", { val: JSON.stringify(this.homeData), ack: true });
			} catch (e: unknown) {
				this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `Error updating HomeData: ${this.adapter.errorStack(e)}`, "error");
				this.homeData = null;
			}
		} else {
			this.adapter.rLog("HTTP", null, "Error", "Cloud", undefined, `No homeId found`, "error");
		}
	}

	/**
	 * Returns the RRIOT authentication data.
	 */
	get_rriot(): RriotData {
		if (!this.userData) {
			throw new Error("this.userData is not initialized. Call updateHomeData() first");
		}
		return this.userData.rriot;
	}

	/**
	 * Resolves the numeric Product ID for a given model.
	 * Tries V3 HomeData first, then V5 ProductInfo.
	 */
	getProductIdByModel(modelName: string): number | null {
		// 1. Try V5 ProductInfo
		if (this.productInfo && this.productInfo.data && Array.isArray(this.productInfo.data.categoryDetailList)) {
			for (const detail of this.productInfo.data.categoryDetailList) {
				if (detail.productList && Array.isArray(detail.productList)) {
					const productV5 = detail.productList.find((p) => p.model === modelName);
					if (productV5) {
						return Number(productV5.id);
					}
				}
			}
		}

		// Log if we have info but can't find model
		if (this.productInfo) {
			this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, `Model ${modelName} not found in V5 ProductInfo.`, "warn");
		}

		return null;
	}

	/**
	 * Retrieves scenes for the current home.
	 */
	async getScenes(): Promise<SceneResponse> {
		if (!this.loginApi) throw new Error("loginApi is not initialized.");
		if (!this.realApi) throw new Error("realApi is not initialized.");

		return await this.realApi.get(`user/scene/home/${this.homeID}`).then((res) => res.data);
	}

	/**
	 * Stores firmware feature IDs in the cache for a specific device.
	 */
	public storeFwFeaturesResult(duid: string, featureIds: number[]): void {
		if (Array.isArray(featureIds)) {
			this.fwFeaturesCache.set(duid, featureIds);
			this.adapter.rLog("HTTP", duid, "Debug", "Cloud", undefined, `Stored FW features result: ${JSON.stringify(featureIds)}`, "debug");
		} else {
			this.adapter.rLog("HTTP", duid, "Warn", "Cloud", undefined, `Invalid data received for storing FW features: ${JSON.stringify(featureIds)}`, "warn");
		}
	}

	public getFwFeaturesResult(duid: string): number[] | undefined {
		return this.fwFeaturesCache.get(duid);
	}

	/**
	 * Retrieves firmware update status for a device.
	 */
	async getFirmwareStates(duid: string): Promise<any> {
		try {
			if (!this.realApi) throw new Error("realApi is not initialized.");

			return await this.realApi.get(`ota/firmware/${duid}/updatev2`);
		} catch (error: unknown) {
			throw new Error(`Error in getFirmwareStates: ${this.adapter.errorMessage(error)}`);
		}
	}

	/**
	 * Returns the list of products from HomeData.
	 */
	getProducts(): Product[] {
		if (!this.homeData) return [];
		return this.homeData.products || [];
	}

	/**
	 * Returns a combined list of owned and shared devices.
	 * Returns an empty array if homeData is not initialized.
	 */
	getDevices(): Device[] {
		if (!this.homeData) {
			return [];
		}
		return [...(this.homeData.devices || []), ...(this.homeData.receivedDevices || [])];
	}

	getReceivedDevices(): Device[] {
		if (!this.homeData) return [];
		return this.homeData.receivedDevices || [];
	}

	/**
	 * Checks if a device is a shared device (not owned by the user).
	 */
	isSharedDevice(duid: string): boolean {
		const sharedDevices = this.getReceivedDevices();
		return sharedDevices.some((device) => device.duid === duid);
	}

	/**
	 * Fetches room list for a shared device (owner's rooms).
	 * GET user/deviceshare/query/{duid}/rooms
	 * @returns Array of { id, name } or [] on error / non-shared
	 */
	async getSharedDeviceRooms(duid: string): Promise<{ id: number; name: string }[]> {
		if (!duid || !this.isSharedDevice(duid)) return [];
		try {
			if (!this.realApi) {
				this.adapter.rLog("HTTP", duid, "Warn", "Cloud", undefined, "getSharedDeviceRooms: realApi not initialized.", "warn");
				return [];
			}
			const res = await this.realApi.get<{ success?: boolean; result?: { id: number; name: string }[] }>(`user/deviceshare/query/${duid}/rooms`);
			const result = res.data?.result;
			if (Array.isArray(result)) return result;
			return [];
		} catch (e: any) {
			this.adapter.rLog("HTTP", duid, "Warn", "Cloud", undefined, `getSharedDeviceRooms failed: ${e?.message || e}`, "warn");
			return [];
		}
	}

	/**
	 * Matches rooms from HomeData and optionally assigns fallback names.
	 */
	getMatchedRoomIDs(assignFallbackNames = false): { id: number; name: string }[] {
		if (!this.homeData || !Array.isArray(this.homeData.rooms)) {
			// Not throwing an error here anymore, just logging warning to prevent crashes if rooms are missing
			this.adapter.rLog("HTTP", null, "Warn", "Cloud", undefined, "getMatchedRoomIDs: this.homeData.rooms is missing or invalid.", "warn");
			return [];
		}

		let unnamedCounter = 1;

		const matchedRooms = this.homeData.rooms.map((room) => {
			let name = room.name?.trim();

			if (!name && assignFallbackNames) {
				name = `Room ${unnamedCounter++}`;
			}

			return {
				id: room.id,
				name: name || "",
			};
		});

		if (assignFallbackNames) {
			this.adapter.rLog("HTTP", null, "Info", "Cloud", undefined, `Matched ${matchedRooms.length} rooms (fallback names included)`, "info");
		}

		return matchedRooms;
	}

	/**
	 * Maps all devices to their local keys.
	 */
	getMatchedLocalKeys(): Map<string, string> {
		const devices = this.getDevices();
		return new Map(devices.map((device) => [device.duid, device.localKey]));
	}

	/**
	 * Finds the model name for a given device DUID.
	 */
	getRobotModel(duid: string): string | null {
		if (!duid) {
			throw new Error("Parameter duid missing in function getRobotModel");
		}

		const devices = this.getDevices();

		try {
			const products = this.getProducts();

			const device = devices.find((d) => d.duid === duid);
			if (!device) {
				this.adapter.rLog("HTTP", duid, "Error", "Cloud", undefined, "Device not found in local homeData", "error");
				return null;
			}

			const product = products.find((p) => p.id === device.productId);
			return product ? product.model : null;
		} catch (error: unknown) {
			this.adapter.rLog("HTTP", duid, "Error", "Cloud", undefined, `Error in getRobotModel: ${this.adapter.errorMessage(error)}`, "error");
			return null;
		}
	}

	/**
	 * Finds the product category for a given device DUID.
	 */
	getProductCategory(duid: string): string | null {
		const devices = this.getDevices();

		try {
			const products = this.getProducts();

			const device = devices.find((d) => d.duid == duid);
			if (!device) return null;

			const product = products.find((p) => p.id == device.productId);
			return product ? product.category : null;
		} catch (error: unknown) {
			this.adapter.rLog("HTTP", duid, "Error", "Cloud", undefined, `Error in getProductCategory: ${this.adapter.errorMessage(error)}`, "error");
			return null;
		}
	}

	public getFeatureSet(duid: string): number | undefined {
		const allDevices = this.getDevices();
		const device = allDevices.find((d) => d.duid === duid);
		return device?.featureSet;
	}

	public getNewFeatureSet(duid: string): string | undefined {
		const allDevices = this.getDevices();
		const device = allDevices.find((d) => d.duid === duid);
		return device?.newFeatureSet;
	}
}
