// src/lib/features/base_device_features.ts
import { z } from "zod";

import type { Roborock } from "../../main";
import { DeviceStateWriter } from "./deviceStateWriter";
import { Feature } from "./features.enum";

// --- Types & Interfaces ---

/**
 * Command object properties.
 */
export type CommandSpec = {
	type: ioBroker.CommonType | "json"; // 'json' type used for internal logic
	def?: any;
	states?: Record<string | number, string>;
	min?: number;
	max?: number;
	unit?: string;
	role?: string;
};

/**
 * Feature implementation function, 'this' context is bound.
 */
export type FeatureImplementation = () => Promise<void> | void;

/**
 * Model-specific configuration.
 */
export interface DeviceModelConfig {
	staticFeatures: Feature[]; // Features this model always has
}

/**
 * Feature class constructor signature.
 */
export type FeatureClassConstructor = new (_dependencies: FeatureDependencies, _duid: string) => BaseDeviceFeatures;

/**
 * Dependencies injected into feature classes.
 */
export interface FeatureDependencies {
	adapter: Roborock;
	config: Roborock["config"];
	http_api: Roborock["http_api"];
	ensureState: Roborock["ensureState"];
	ensureFolder: Roborock["ensureFolder"];
	log: Roborock["log"];
	// Add other dependencies if needed
}

// --- Registry & Decorator ---

/** Maps robotModelId to feature class constructors. */
const modelRegistry = new Map<string, FeatureClassConstructor>();

/**
 * Decorator to register a feature class for a robot model.
 * @param robotModelId Unique model identifier (e.g. 'roborock.vacuum.a70').
 */
export function RegisterModel(robotModelId: string) {
	return function (constructor: FeatureClassConstructor) {
		if (modelRegistry.has(robotModelId)) {
			// Model already registered, overwriting.
		}
		modelRegistry.set(robotModelId, constructor);
	};
}

// --- Zod Schemas (Base) ---

/**
 * Base Zod schema for generic status properties.
 */
export const BaseStatusSchema = z.looseObject({
	error_code: z.number().int().optional(),
	// Add generic status fields if applicable
});

// --- Generic Base Class ---

/**
 * Base class for device features. Handles init, feature application, and commands.
 * Extended by specific types (e.g. V1VacuumFeatures).
 */
export abstract class BaseDeviceFeatures {
	protected createdStates: Set<string> = new Set(); // Track created states to avoid redundant ensureState calls
	protected runtimeDetectionComplete = false; // Initial runtime detection flag
	protected readonly stateWriter: DeviceStateWriter;

	protected deps: FeatureDependencies;
	public commands: Record<string, CommandSpec | any>; // Command definitions for this device
	public extraCommandGroups: Record<string, Record<string, CommandSpec | any>>;
	protected duid: string;
	protected robotModel: string;
	public protocolVersion: string | null = null;
	protected config: DeviceModelConfig; // Static feature config from model class
	protected appliedFeatures = new Set<Feature>(); // Tracks applied features
	protected pendingFeatures = new Set<Feature>(); // Tracks features currently being applied (Race Condition Guard)
	protected commandsCreated = false; // Command objects created flag

	// --- Constants (Generic) ---
	protected static readonly CONSTANTS = {
		// Generic constants for all Roborock devices
		baseCommands: {},
		// Generic error codes (subset)
		errorCodes: {
			0: "No error",
			255: "Internal error",
			"-1": "Unknown Error",
			// Add more if generic across all devices
		},
	};

	// --- Metadata Key for Feature Registry ---
	// Unique symbol for registry on prototype
	public static readonly FEATURE_METADATA_KEY = Symbol.for("roborock.featureRegistry");

	/**
	 * Decorator to register a feature handler method.
	 * @param feature The Feature enum key.
	 */
	public static DeviceFeature(feature: Feature) {
		return function (target: any, propertyKey: string) {
			// 'target' is the prototype
			let registry: Map<Feature, string> = target[BaseDeviceFeatures.FEATURE_METADATA_KEY];
			if (!registry) {
				registry = new Map();
				// Store on prototype
				target[BaseDeviceFeatures.FEATURE_METADATA_KEY] = registry;
			}
			registry.set(feature, propertyKey);
		};
	}

	// --- Feature Registry (Instance Based via Metadata) ---

	/**
	 * Base feature handler constructor.
	 * @param dependencies Injected dependencies.
	 * @param duid Device unique identifier.
	 * @param robotModel Robot model string.
	 * @param config Static feature config.
	 */
	constructor(dependencies: FeatureDependencies, duid: string, robotModel: string, config: DeviceModelConfig) {
		this.deps = dependencies;
		this.duid = duid;
		this.robotModel = robotModel;
		this.config = config;
		this.stateWriter = new DeviceStateWriter(dependencies, duid);
		// Initialize empty commands map. Actual commands will be populated during setupProtocolFeatures.
		this.commands = {};
		this.extraCommandGroups = {};
	}

	/**
	 * Applies a feature if not already applied. Looks up implementation in registry.
	 * @param feature Feature enum key.
	 * @returns `true` if applied now.
	 */
	protected async applyFeature(feature: Feature): Promise<boolean> {
		// Validate input feature
		if (!feature || !Object.values(Feature).includes(feature)) {
			this.deps.log.warn(`[${this.duid}] Attempted to apply invalid feature value: ${feature}`);
			return false;
		}
		// Check if already applied or pending
		if (this.appliedFeatures.has(feature) || this.pendingFeatures.has(feature)) {
			return false;
		}

		// Get registry from instance metadata (prototype chain)
		const registry: Map<Feature, string> | undefined = (this as any)[BaseDeviceFeatures.FEATURE_METADATA_KEY];

		if (registry && registry.has(feature)) {
			const methodName = registry.get(feature)!;
			this.pendingFeatures.add(feature); // Lock
			try {
				const applyMethod = (this as unknown as Record<string, () => Promise<void>>)[methodName];
				if (typeof applyMethod !== "function") throw new Error(`Feature ${String(feature)}: missing method ${methodName}`);
				await applyMethod.call(this);
				this.appliedFeatures.add(feature); // Mark applied after success
				return true;
			} catch (e: unknown) {
				const stack = e instanceof Error ? e.stack : "";
				this.deps.log.error(`[FeatureApply|${this.robotModel}|${this.duid}] Error applying feature '${feature}': ${this.deps.adapter.errorMessage(e)} ${stack}`);
				return false;
			} finally {
				this.pendingFeatures.delete(feature); // Unlock
			}
		} else {
			return false;
		}
	}

	// --- Abstract / Overridable Methods ---

	/**
	 * Detects features via device-specific mechanisms (bitfields, fw info).
	 * Implemented by subclasses.
	 * @returns Set of detected `Feature` enum keys.
	 */
	protected abstract getDynamicFeatures(): Set<Feature>;

	/**
	 * Applies static features from config.
	 * Override for pre-runtime model logic.
	 * @param _statusData Optional initial status data.
	 * @param _fwFeatures Optional initial firmware features.
	 */
	public async applyModelSpecifics(): Promise<void> {
		const promises = this.config.staticFeatures.map((feature) => this.applyFeature(feature));
		await Promise.all(promises);
	}

	/**
	 * Performs runtime feature detection using status data.
	 * Implemented by subclasses.
	 * @param statusData Validated status data.
	 * @param fwFeatures Optional firmware features.
	 * @returns `true` if features/commands changed.
	 */
	public abstract detectAndApplyRuntimeFeatures(_statusData: Readonly<Record<string, any>>): Promise<boolean>;

	// --- Core Initialization Logic ---

	/**
	 * Initializes features: Model Specifics -> Runtime Detection -> Dock Processing -> Command Objects.
	 * @param initialStatus Optional initial status.
	 * @param initialFwFeatures Optional initial firmware features.
	 */
	public async initialize(online: boolean = false): Promise<void> {
		// Flow: Protocol -> Model Specifics -> Runtime Detection -> Dock Processing -> Command Objects

		// 0. Setup Protocol Features (Command Sets)
		try {
			await this.setupProtocolFeatures();
		} catch (e: unknown) {
			this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error setting up protocol features: ${this.deps.adapter.errorMessage(e)}`, "error");
		}

		// 1. Apply Model Specifics
		try {
			await this.applyModelSpecifics();
		} catch (e: unknown) {
			this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error applying model specifics: ${this.deps.adapter.errorMessage(e)}`, "error");
		}

		// 2. Create/Update ioBroker Objects (Commands)
		// Must be done BEFORE fetching data, as data updates might sync to command states.
		try {
			await this.createCommandObjects();
		} catch (e: unknown) {
			this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error creating command objects: ${this.deps.adapter.errorMessage(e)}`, "error");
		}

		// 3. Fetch initial data if online
		if (online) {
			try {
				await this.initializeDeviceData();
			} catch (e: unknown) {
				this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error initializing device data: ${this.deps.adapter.errorMessage(e)}`, "error");
			}
		}
	}

	/**
	 * Fetches initial runtime data (status, consumables, map).
	 */
	public async initializeDeviceData(): Promise<void> {
		// Default implementation: update status if online
		await this.updateStatus();
		await this.updateFirmwareFeatures();
		await this.updateMap();
	}

	public async setupProtocolFeatures(): Promise<void> {
		// Initialize with generic base commands
		this.commands = JSON.parse(JSON.stringify(BaseDeviceFeatures.CONSTANTS.baseCommands));
		this.extraCommandGroups = {};
	}

	/**
	 * Logs summary of applied features and commands. Call after init.
	 */
	public printSummary(): void {
	}

	// --- Core Helper Methods ---

	/**

	 * Maps dynamic feature keys (e.g. 'is...') to action keys (e.g. 'MopWash').
	 * @param detectedFeature Detected Feature enum key.
	 * @returns Mapped action Feature key, detected key if actionable, or null.
	 */
	protected mapFeature(detectedFeature: Feature): Feature | null {
		// Get registry from instance metadata
		const registry: Map<Feature, string> | undefined = (this as any)[BaseDeviceFeatures.FEATURE_METADATA_KEY];

		// Check if 'is...' key value exists as enum key
		const potentialActionName = Feature[detectedFeature as keyof typeof Feature];
		// Find enum key for string value, excluding original key
		const mappedActionKey = (Object.keys(Feature) as Array<keyof typeof Feature>).find((key) => Feature[key] === potentialActionName && key !== detectedFeature);

		if (mappedActionKey) {
			const actionFeatureEnum = Feature[mappedActionKey];
			// Check if mapped action has registered implementation
			if (registry && registry.has(actionFeatureEnum)) {
				this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Mapping dynamic feature '${detectedFeature}' to action '${actionFeatureEnum}'`, "debug");
				return actionFeatureEnum;
			} else {
				this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Dynamic feature '${detectedFeature}' mapped to '${actionFeatureEnum}', but no action registered.`, "debug");
				return null;
			}
		}

		// Check if detected feature has registered action
		if (registry && registry.has(detectedFeature)) {
			this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Using dynamic feature '${detectedFeature}' directly.`, "debug");
			return detectedFeature;
		}

		// No mapping or action found
		this.deps.adapter.rLog("System", this.duid, "Debug", undefined, undefined, `Dynamic feature '${detectedFeature}' detected but has no registered action or mapping.`, "debug");
		return null;
	}

	/**
	 * Creates/updates ioBroker command objects from this.commands.
	 */
	public async createCommandObjects(): Promise<void> {
		const commandGroups: Record<string, Record<string, CommandSpec | any>> = {
			commands: this.commands,
			...this.extraCommandGroups
		};

		const promises: Promise<void>[] = [];
		for (const [folderName, groupCommands] of Object.entries(commandGroups)) {
			const folderPath = `Devices.${this.duid}.${folderName}`;
			try {
				await this.deps.ensureFolder(folderPath);
			} catch (e: unknown) {
				this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Failed to ensure commands folder ${folderPath}: ${this.deps.adapter.errorMessage(e)}`, "error");
				return;
			}

			for (const [command, commonCommand] of Object.entries(groupCommands)) {
				promises.push(this.processCommand(folderPath, command, commonCommand));
			}
		}

		try {
			await Promise.all(promises); // Wait for all operations
			this.commandsCreated = true; // Done
		} catch (e: unknown) {
			// Catch Promise.all errors (rare)
			this.deps.log.error(`[${this.duid}] Critical error during parallel command object creation: ${this.deps.adapter.errorMessage(e)}`);
		}
	}

	/**
	 * Process a single command object creation.
	 */
	protected async processCommand(folderPath: string, cmd: string, spec: CommandSpec | any): Promise<void> {
		try {
			const options: Partial<ioBroker.StateCommon> = {
				...(spec as Partial<ioBroker.StateCommon>),
				name: spec.name || this.deps.adapter.translations[cmd] || cmd, // Add name generation
				write: true, // Writable
			};
			const originalType = spec.type; // Store original type

			// Determine Role
			if (!options.role) {
				if (originalType === "boolean" && !options.states) options.role = "button";
				else if (originalType === "number" && options.states) options.role = "value.list";
				else if (originalType === "number") options.role = "level";
				else if (originalType === "json" && options.states) options.role = "value.list";
				else if (originalType === "json") options.role = "json";
				else options.role = "state";
			}

			// Enforce default value if missing (User requirement: no null defaults)
			if (options.def === undefined || options.def === null) {
				if (options.type === "boolean" || options.role === "button") {
					options.def = false;
				} else if (options.type === "number") {
					options.def = options.min ?? 0;
				} else if (options.type === "string") {
					options.def = "";
				}
			}

			// Adjust type
			if (originalType === "json") {
				options.type = "string";
			}

			// Type validation and default
			const validTypes: ioBroker.CommonType[] = ["string", "number", "boolean", "object", "array", "mixed"];
			if (!options.type || typeof options.type !== "string" || !validTypes.includes(options.type as ioBroker.CommonType)) {
				if (originalType !== "json") {
					// Skip log if setting to string
					this.deps.log.warn(`[${this.duid}] Invalid or missing type '${spec.type}' for command '${cmd}', defaulting to 'string'.`);
				}
				options.type = "string";
			}

			const path = `${folderPath}.${cmd}`;

			// Create/Update Object
			const existingObj = await this.deps.adapter.getObjectAsync(path);
			if (existingObj) {
				// Extend if common differs. Stringify is good enough for now.
				if (JSON.stringify(existingObj.common) !== JSON.stringify(options)) {
					await this.deps.adapter.extendObject(path, { common: options as ioBroker.StateCommon });
				}
			} else {
				await this.deps.ensureState(path, options as ioBroker.StateCommon);
			}

			// Reset button states
			if (options.role === "button" && options.type === "boolean") {
				const currentState = await this.deps.adapter.getStateAsync(path);
				// Reset to false if needed
				if (!currentState || currentState.val !== false) {
					await this.deps.adapter.setState(path, false, true);
				}
			}
		} catch (e: unknown) {
			this.deps.log.error(`[${this.duid}] Error processing command object '${cmd}': ${this.deps.adapter.errorMessage(e)}`);
		}
	}

	// --- Helper Methods ---

	/**
	 * Adds/updates command definition. Merges states to preserve specifics.
	 * @param name Command name.
	 * @param spec CommandSpec definition.
	 */
	protected addCommand(name: string, spec: CommandSpec | any, group = "commands"): void {
		if (!name || typeof name !== "string") {
			this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `addCommand: Invalid command name provided: ${name}`, "error");
			return;
		}
		try {
			let targetGroup = this.commands;
			if (group !== "commands") {
				if (!this.extraCommandGroups[group]) {
					this.extraCommandGroups[group] = {};
				}
				targetGroup = this.extraCommandGroups[group];
			}
			// Merge states if new spec has fewer states.
			if (targetGroup[name]?.states && spec.states) {
				const existingStatesJson = JSON.stringify(targetGroup[name].states);
				const newStatesJson = JSON.stringify(spec.states);
				if (existingStatesJson !== newStatesJson) {
					// Merge: New states overwrite/add
					spec.states = { ...targetGroup[name].states, ...spec.states };
				} else {
					// Preserve existing spec if states identical
					spec = { ...targetGroup[name], ...spec, states: targetGroup[name].states };
				}
			} else if (targetGroup[name]?.states && !spec.states) {
				// Keep existing states if new one has none
				spec.states = targetGroup[name].states;
			}
			targetGroup[name] = spec;
		} catch (e: unknown) {
			this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error in addCommand for '${name}': ${this.deps.adapter.errorMessage(e)}`, "error");
		}
	}

	public getCommandFolders(): string[] {
		return ["commands", ...Object.keys(this.extraCommandGroups)];
	}

	public hasCommandFolder(folder: string): boolean {
		return folder === "commands" || Object.prototype.hasOwnProperty.call(this.extraCommandGroups, folder);
	}

	public getCommandSpec(folder: string, command: string): CommandSpec | any | undefined {
		if (folder === "commands") {
			return this.commands[command];
		}

		return this.extraCommandGroups[folder]?.[command];
	}

	/**
	 * Calls injected ensureState with correct path.
	 * @param subfolder Subfolder name.
	 * @param stateName State name.
	 * @param commonOptions State options.
	 * @param native Optional native options.
	 */
	protected async ensureState(subfolder: string, stateName: string, commonOptions: Partial<ioBroker.StateCommon>, native: Record<string, any> = {}): Promise<void> {
		const path = `Devices.${this.duid}.${subfolder}.${stateName}`;
		try {
			// Validate type before ensureState
			const validTypes: ioBroker.CommonType[] = ["string", "number", "boolean", "object", "array", "mixed"];
			if (commonOptions.type && !validTypes.includes(commonOptions.type as ioBroker.CommonType)) {
				this.deps.adapter.rLog("System", this.duid, "Warn", undefined, undefined, `Invalid type '${commonOptions.type}' in ensureState for ${path}, defaulting to 'string'.`, "warn");
				commonOptions.type = "string";
			}

			// Check if object exists and needs update
			const existingObj = await this.deps.adapter.getObjectAsync(path);
			if (existingObj && existingObj.common && this.hasStatesChanged(commonOptions.states, existingObj.common.states)) {
				this.deps.log.debug(`[${this.duid}] Updating object definition for ${path} (states mapping changed)`);
				await this.deps.adapter.extendObject(path, {
					common: commonOptions as ioBroker.StateCommon,
					native: native
				});
				return;
			}

			// Standard ensure (creates if not exists)
			await this.deps.ensureState(path, commonOptions as ioBroker.StateCommon, native); // Cast after validation
		} catch (e: unknown) {
			this.deps.adapter.rLog("System", this.duid, "Error", undefined, undefined, `Error in ensureState for ${path}: ${this.deps.adapter.errorMessage(e)}`, "error");
		}
	}

	// --- Static Methods ---

	/**
	 * Get registered feature class for model.
	 * @param modelId Robot model identifier.
	 * @returns Constructor or undefined.
	 */
	public static getRegisteredModelClass(modelId: string): FeatureClassConstructor | undefined {
		return modelRegistry.get(modelId);
	}

	/**
	 * Get all registered model IDs.
	 */
	public static getRegisteredModels(): string[] {
		return Array.from(modelRegistry.keys());
	}

	/**
	 * Check if static feature is defined.
	 * @param feature Feature enum key.
	 */
	public hasStaticFeature(feature: Feature): boolean {
		return this.config.staticFeatures.includes(feature);
	}

	public hasFeature(feature: Feature): boolean {
		return this.appliedFeatures.has(feature) || this.config.staticFeatures.includes(feature);
	}

	/**
	 * Helper to safely access dynamic feature methods.
	 * Encapsulates type casting for readability.
	 */
	protected getFeatureMethod(name: string): Function {
		// Safe access using keyof assertion
		const method = this[name as keyof this];
		if (typeof method === "function") {
			return method as Function;
		}
		throw new Error(`Feature method '${name}' not found or is not a function.`);
	}

	// --- Command Parameter Interception ---

	/**
	 * Allows feature handlers to provide/modify parameters for a command before sending.
	 * Override this to implement logic like 'app_segment_clean' gathering segments from states.
	 * @param method Command method name.
	 * @param params Existing parameters passed from caller.
	 */
	public async getCommandParams(method: string, params?: unknown, id?: string): Promise<unknown> {
		void method;
		void id;
		return params;
	}

	public async onCommandResult(requestedMethod: string, finalMethod: string, response: unknown, params?: unknown): Promise<void> {
		void requestedMethod;
		void finalMethod;
		void response;
		void params;
	}

	// --- Data Update Methods (Unified Data Handling) ---

	/**
	 * Fetch data and store in folder.
	 * @param method API method.
	 * @param params API parameters.
	 * @param folder Target folder.
	 * @param mapper Optional data mapper.
	 */
	protected async requestAndProcess(method: string, params: any[], folder: string, mapper?: (data: any) => Record<string, any> | Promise<Record<string, any>>): Promise<void> {
		try {
			const result = await this.deps.adapter.requestsHandler.sendRequest(this.duid, method, params);

			let resultObj: Record<string, unknown> | undefined;

			// Recursively unwrap single-element arrays (common in B01/Tuya responses)
			let unwrapped = result;
			while (Array.isArray(unwrapped) && unwrapped.length === 1) {
				unwrapped = unwrapped[0];
			}

			if (typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped)) {
				resultObj = unwrapped as Record<string, unknown>;
			}

			if (resultObj) {
				// Apply mapper
				if (mapper) {
					resultObj = await mapper(resultObj);
				}

				await this.deps.ensureFolder(`Devices.${this.duid}.${folder}`);

				for (const key in resultObj) {
					await this.processResultKey(folder, key, resultObj[key]);
				}
			}
		} catch (e: unknown) {
			this.deps.adapter.rLog("System", this.duid, "Warn", undefined, undefined, `Failed to update ${folder} (method: ${method}): ${this.deps.adapter.errorMessage(e)}`, "warn");
		}
	}

	/**
	 * Process a single key from API result.
	 */
	protected async processResultKey(folder: string, key: string, val: unknown): Promise<void> {
		// Determine common options (type, role, unit)
		let common: Partial<ioBroker.StateCommon> | undefined;
		if (folder === "deviceStatus") {
			common = this.getCommonDeviceStates(key);
		} else if (folder === "cleaningInfo") {
			common = this.getCommonCleaningInfo(key);
		} else if (folder === "cleaningRecords" || folder.includes("records")) {
			common = this.getCommonCleaningRecords(key);
		}

		if (!common) {
			common = { name: key, type: typeof val as ioBroker.CommonType, read: true, write: false };
		}

		// Handle Objects/Arrays by stringifying them so they don't crash the state
		if (typeof val === "object" && val !== null) {
			val = JSON.stringify(val);
		}

		// Formatting for timestamp keys only (clean_finish is 0/1 flag, not a timestamp)
		if ((key === "last_clean_t" || key === "begin" || key === "end") && typeof (val as any) === "number") {
			val = new Date((val as number) * 1000).toLocaleString();
			common.type = "string"; // Update type to match new value
		}

		// Enforce type matching to keep the log clean
		if (common.type === "string" && typeof val !== "string") {
			val = String(val);
		} else if (common.type === "number" && typeof val !== "number") {
			val = Number(val);
		} else if (common.type === "boolean" && typeof val !== "boolean") {
			val = !!val;
		}

		const fullPath = `Devices.${this.duid}.${folder}.${key}`;

		if (!this.createdStates.has(fullPath)) {
			await this.deps.ensureState(fullPath, common);
			this.createdStates.add(fullPath);
		}

		await this.deps.adapter.setStateChanged(fullPath, { val: val as ioBroker.StateValue, ack: true });
	}

	// --- Helper Methods ---

	private hasStatesChanged(
		newStates: Record<string, string> | string | string[] | undefined,
		oldStates: Record<string, string> | string | string[] | undefined
	): boolean {
		if (!!newStates !== !!oldStates) return true; // One is defined, one is not
		if (!newStates || !oldStates) return false; // Both undefined
		return JSON.stringify(newStates) !== JSON.stringify(oldStates);
	}

	public async updateStatus(): Promise<void> {
		// Default for vacuums
		await this.requestAndProcess("get_prop", ["get_status"], "deviceStatus");
	}

	public async updateConsumables(): Promise<void> {
		if (!this.hasFeature(Feature.Consumables)) return;
		await this.requestAndProcess("get_consumable", [], "consumables");
	}

	public async updateNetworkInfo(): Promise<void> {
		// No feature guard: get_network_info is supported on all devices (V1/MQTT); B01 overrides and uses service.get_net_info.
		await this.requestAndProcess("get_network_info", [], "networkInfo");
	}

	public async updateTimers(): Promise<void> {
		if (!this.hasFeature(Feature.Timers)) return;
		await this.requestAndProcess("get_timer", [], "timers");
		await this.requestAndProcess("get_server_timer", [], "timers");
	}

	public async updateFirmwareFeatures(): Promise<void> {
		if (!this.hasFeature(Feature.FirmwareInfo)) return;
		await this.requestAndProcess("get_fw_features", [], "firmwareFeatures");
	}

	public async updateMultiMapsList(): Promise<void> {
		if (!this.hasFeature(Feature.MultiMap)) return;
		await this.requestAndProcess("get_multi_maps_list", [], "map");
	}

	public async updateRoomMapping(): Promise<void> {
		if (!this.hasFeature(Feature.RoomMapping)) return;
		await this.requestAndProcess("get_room_mapping", [], "map");
	}

	// Complex updates (override in subclasses)
	public async updateCleanSummary(): Promise<void> {
		// Default: no-op
	}

	public async updateMap(): Promise<void> {
		// Default: no-op
	}

	public async updateExtraStatus(): Promise<void> {
		// Default: no-op. Override for model-specifics.
	}

	public getCurrentMapIndex(): number {
		return 0;
	}

	public async getPhoto(imgId: string, type: number): Promise<any> {
		if (!this.hasFeature(Feature.GetPhoto)) {
			throw new Error("getPhoto feature not enabled for this device");
		}

		try {
			const res = (await this.deps.adapter.requestsHandler.sendRequest(
				this.duid,
				"get_photo",
				{
					data_filter: {
						img_id: imgId,
						type: type,
					},
				}
			)) as any;
			// PhotoManager handles the async 300/301 packets and resolves the promise with the final image.
			// The data returned here is the result of that resolution.
			// If the robot supports encryption (Cipher 1), PhotoManager now automatically handles RSA/AES decryption.
			const responseData = (res as any).buffer ? res : (res as any).data || res;

			return responseData;
		} catch (e: unknown) {
			this.deps.adapter.rLog("Requests", this.duid, "Error", this.protocolVersion || undefined, undefined, `[getPhoto] Failed: ${this.deps.adapter.errorMessage(e)}`, "error");
			throw e;
		}
	}

	// --- Instance Getters for Constants (Abstract Declarations) ---
	// Implemented by subclasses to provide constants.

	public abstract getCommonConsumable(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
	public abstract isResetableConsumable(consumable: string): boolean;
	public abstract getCommonDeviceStates(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
	public abstract getCommonCleaningRecords(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
	public abstract getFirmwareFeatureName(featureID: string | number): string;
	public abstract getCommonCleaningInfo(attribute: string | number): Partial<ioBroker.StateCommon> | undefined;
}
