import type { Characteristic, CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
import type { WithUUID } from 'hap-nodejs';
import type { KoboldRobot } from 'node-kobold-control';

import Debug from 'debug';
import 'colors';

import type { KoboldBoundary, KoboldHomebridgePlatform, RobotRecord } from './platform.js';

const debug = Debug('homebridge-kobold');

const dictionaries = {
  en: {
    clean: 'Clean',
    'clean the': 'Clean the',
    goToDock: 'Go to Dock',
    dockState: 'Dock',
    eco: 'Eco Mode',
    noGoLines: 'NoGo Lines',
    extraCare: 'Extra Care',
    schedule: 'Schedule',
    findMe: 'Find me',
    cleanSpot: 'Clean Spot',
    battery: 'Battery',
  },
  de: {
    clean: 'Sauge',
    'clean the': 'Sauge',
    goToDock: 'Zur Basis',
    dockState: 'In der Basis',
    eco: 'Eco Modus',
    noGoLines: 'NoGo Linien',
    extraCare: 'Extra Care',
    schedule: 'Zeitplan',
    findMe: 'Finde mich',
    cleanSpot: 'Spot Reinigung',
    battery: 'Batterie',
  },
  fr: {
    clean: 'Aspirer',
    'clean the': 'Aspirer',
    goToDock: 'Retour à la base',
    dockState: 'Sur la base',
    eco: 'Eco mode',
    noGoLines: 'Lignes NoGo',
    extraCare: 'Extra Care',
    schedule: 'Planifier',
    findMe: 'Me retrouver',
    cleanSpot: 'Nettoyage local',
    battery: 'Batterie',
  },
} as const;

type Dictionary = (typeof dictionaries)[keyof typeof dictionaries];

interface SpotSettings {
  width: number | null;
  height: number | null;
  repeat: boolean;
}

type HapServiceConstructor = (typeof Service) & WithUUID<typeof Service>;
type HapCharacteristicConstructor = WithUUID<new () => Characteristic>;

export class KoboldVacuumAccessory {
  private readonly robotObject: RobotRecord;
  private readonly robot: KoboldRobot;
  private readonly meta: Record<string, unknown>;
  private readonly dict: Dictionary;

  private readonly cleanService: Service;
  private readonly batteryService?: Service;
  private readonly goToDockService?: Service;
  private readonly dockStateService?: Service;
  private readonly ecoService?: Service;
  private readonly noGoLinesService?: Service;
  private readonly extraCareService?: Service;
  private readonly scheduleService?: Service;
  private readonly findMeService?: Service;
  private readonly spotCleanService?: Service;

  private spotWidthCharacteristic?: Characteristic;
  private spotHeightCharacteristic?: Characteristic;
  private spotRepeatCharacteristic?: Characteristic;

  private readonly spotPlusFeatures: boolean;
  private readonly refreshSetting;
  private nextRoom: string | null = null;
  private readonly name: string;
  private readonly boundaryServices: Map<string, Service> = new Map();
  private readonly boundaryLabels: Map<string, string> = new Map();
  private readonly pendingBoundaryStates: Map<string, boolean | null> = new Map();

  constructor(
    private readonly platform: KoboldHomebridgePlatform,
    private readonly accessory: PlatformAccessory,
    robotObject: RobotRecord,
  ) {
    this.robotObject = robotObject;
    this.robot = robotObject.device;
    this.meta = robotObject.meta;
    this.refreshSetting = this.platform.refresh;

    this.dict = dictionaries[this.platform.language as keyof typeof dictionaries] ?? dictionaries.en;
    this.spotPlusFeatures = Array.isArray(this.robotObject.availableServices?.spotCleaning)
      ? this.robotObject.availableServices.spotCleaning.includes('basic')
      : false;

    this.name = this.robot.name;
    this.accessory.displayName = this.name;
    this.accessory.context.displayName = this.name;
    this.accessory.context.robotSerial = this.robot._serial;
    this.accessory.context.boundaryId = null;

    const modelName = typeof this.meta.modelName === 'string' ? this.meta.modelName : 'Unknown Model';
    const firmware = typeof this.meta.firmware === 'string' ? this.meta.firmware : 'Unknown';

    this.informationService()
      .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Vorwerk Deutschland Stiftung & Co. KG')
      .setCharacteristic(this.platform.Characteristic.Model, modelName)
      .setCharacteristic(this.platform.Characteristic.SerialNumber, this.robot._serial)
      .setCharacteristic(this.platform.Characteristic.FirmwareRevision, firmware)
      .setCharacteristic(this.platform.Characteristic.Name, this.robot.name);

    this.cleanService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.clean}`,
      'clean',
    );

    this.cleanService.getCharacteristic(this.platform.Characteristic.On)
      .onSet(value => this.setClean(value))
      .onGet(() => this.getClean());

    this.batteryService = this.createService(
      this.platform.Service.Battery,
      `${this.name} ${this.dict.battery}`,
      'battery',
      true,
    );

    if (this.batteryService) {
      this.batteryService.getCharacteristic(this.platform.Characteristic.BatteryLevel)
        .onGet(this.getBatteryLevel.bind(this));
      this.batteryService.getCharacteristic(this.platform.Characteristic.ChargingState)
        .onGet(this.getBatteryChargingState.bind(this));
    }

    
    const exposeDock = !this.platform.isServiceHidden('dock');
    this.goToDockService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.goToDock}`,
      'goToDock',
      exposeDock,
    );
    if (exposeDock && this.goToDockService) {
      this.goToDockService.getCharacteristic(this.platform.Characteristic.On)
        .onSet(this.setGoToDock.bind(this))
        .onGet(this.getGoToDock.bind(this));
    }

    const exposeDockState = !this.platform.isServiceHidden('dockstate');
    this.dockStateService = this.createService(
      this.platform.Service.OccupancySensor,
      `${this.name} ${this.dict.dockState}`,
      'dockState',
      exposeDockState,
    );
    if (exposeDockState && this.dockStateService) {
      this.dockStateService.getCharacteristic(this.platform.Characteristic.OccupancyDetected)
        .onGet(this.getDock.bind(this));
    }

    const exposeEco = !this.platform.isServiceHidden('eco');
    this.ecoService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.eco}`,
      'eco',
      exposeEco,
    );
    if (exposeEco && this.ecoService) {
      this.ecoService.getCharacteristic(this.platform.Characteristic.On)
        .onSet(this.setEco.bind(this))
        .onGet(this.getEco.bind(this));
    }

    const exposeNoGo = !this.platform.isServiceHidden('nogolines');
    this.noGoLinesService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.noGoLines}`,
      'noGoLines',
      exposeNoGo,
    );
    if (exposeNoGo && this.noGoLinesService) {
      this.noGoLinesService.getCharacteristic(this.platform.Characteristic.On)
        .onSet(this.setNoGoLines.bind(this))
        .onGet(this.getNoGoLines.bind(this));
    }

    const exposeExtraCare = !this.platform.isServiceHidden('extracare');
    this.extraCareService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.extraCare}`,
      'extraCare',
      exposeExtraCare,
    );
    if (exposeExtraCare && this.extraCareService) {
      this.extraCareService.getCharacteristic(this.platform.Characteristic.On)
        .onSet(this.setExtraCare.bind(this))
        .onGet(this.getExtraCare.bind(this));
    }

    const exposeSchedule = !this.platform.isServiceHidden('schedule');
    this.scheduleService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.schedule}`,
      'schedule',
      exposeSchedule,
    );
    if (exposeSchedule && this.scheduleService) {
      this.scheduleService.getCharacteristic(this.platform.Characteristic.On)
        .onSet(this.setSchedule.bind(this))
        .onGet(this.getSchedule.bind(this));
    }

    const exposeFind = !this.platform.isServiceHidden('find');
    this.findMeService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.findMe}`,
      'findMe',
      exposeFind,
    );
    if (exposeFind && this.findMeService) {
      this.findMeService.getCharacteristic(this.platform.Characteristic.On)
        .onSet(this.setFindMe.bind(this))
        .onGet(this.getFindMe.bind(this));
    }

    const exposeSpot = !this.platform.isServiceHidden('spot');
    this.spotCleanService = this.createService(
      this.platform.Service.Switch,
      `${this.name} ${this.dict.cleanSpot}`,
      'cleanSpot',
      exposeSpot,
    );

    if (exposeSpot && this.spotCleanService) {
      this.spotCleanService.getCharacteristic(this.platform.Characteristic.On)
        .onSet(this.setSpotClean.bind(this))
        .onGet(this.getSpotClean.bind(this));

      this.spotRepeatCharacteristic = this.getOrAddCharacteristic(
        this.spotCleanService,
        this.platform.spotCharacteristics.SpotRepeatCharacteristic,
      );
      this.spotRepeatCharacteristic
        ?.onSet(this.setSpotRepeat.bind(this))
        .onGet(this.getSpotRepeat.bind(this));

      if (this.spotPlusFeatures) {
        this.spotWidthCharacteristic = this.getOrAddCharacteristic(
          this.spotCleanService,
          this.platform.spotCharacteristics.SpotWidthCharacteristic,
        );
        this.spotHeightCharacteristic = this.getOrAddCharacteristic(
          this.spotCleanService,
          this.platform.spotCharacteristics.SpotHeightCharacteristic,
        );

        this.spotWidthCharacteristic
          ?.onSet(this.setSpotWidth.bind(this))
          .onGet(this.getSpotWidth.bind(this));
        this.spotHeightCharacteristic
          ?.onSet(this.setSpotHeight.bind(this))
          .onGet(this.getSpotHeight.bind(this));
      }
    }

    this.setupBoundaryServices();
  }

  updated(): void {
    const currentClean = this.cleanService.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
    const robotCanPause = !!this.robot.canPause;
    if ((currentClean ?? false) !== robotCanPause) {
      this.cleanService.updateCharacteristic(this.platform.Characteristic.On, robotCanPause);
    }

    if (this.goToDockService) {
      const dockValue = this.goToDockService.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
      if (dockValue === true && !!this.robot.dockHasBeenSeen) {
        this.goToDockService.updateCharacteristic(this.platform.Characteristic.On, false);
      }
    }

    if (this.scheduleService) {
      const scheduleValue = this.scheduleService.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
      const scheduleEnabled = !!this.robot.isScheduleEnabled;
      if ((scheduleValue ?? false) !== scheduleEnabled) {
        this.scheduleService.updateCharacteristic(
          this.platform.Characteristic.On,
          scheduleEnabled,
        );
      }
    }

    const isDocked = !!this.robot.isDocked;
    this.dockStateService?.updateCharacteristic(
      this.platform.Characteristic.OccupancyDetected,
      isDocked ? 1 : 0,
    );

    this.ecoService?.updateCharacteristic(this.platform.Characteristic.On, !!this.robot.eco);
    this.noGoLinesService?.updateCharacteristic(this.platform.Characteristic.On, !!this.robot.noGoLines);
    this.extraCareService?.updateCharacteristic(
      this.platform.Characteristic.On,
      this.robot.navigationMode === 2,
    );

    const repeatValue = this.robot.spotRepeat ?? false;
    this.spotRepeatCharacteristic?.updateValue(repeatValue);

    if (this.spotPlusFeatures && this.spotWidthCharacteristic && this.spotHeightCharacteristic) {
      const widthProps = this.spotWidthCharacteristic.props;
      const heightProps = this.spotHeightCharacteristic.props;

      const widthValid = this.robot.spotWidth !== undefined
        && this.robot.spotWidth >= (widthProps.minValue ?? 0)
        && this.robot.spotWidth <= (widthProps.maxValue ?? Number.MAX_SAFE_INTEGER)
        ? this.robot.spotWidth
        : widthProps.minValue ?? this.robot.spotWidth ?? widthProps.minValue ?? 0;

      const heightValid = this.robot.spotHeight !== undefined
        && this.robot.spotHeight >= (heightProps.minValue ?? 0)
        && this.robot.spotHeight <= (heightProps.maxValue ?? Number.MAX_SAFE_INTEGER)
        ? this.robot.spotHeight
        : heightProps.minValue ?? this.robot.spotHeight ?? heightProps.minValue ?? 0;

      this.spotWidthCharacteristic.updateValue(widthValid);
      this.spotHeightCharacteristic.updateValue(heightValid);
    }

    this.boundaryServices.forEach((service, boundaryId) => {
      const isCleaningBoundary = !!this.robot.canPause && this.robot.cleaningBoundaryId === boundaryId;
      const boundaryValue = service.getCharacteristic(this.platform.Characteristic.On).value as boolean | undefined;
      if ((boundaryValue ?? false) !== isCleaningBoundary) {
        service.updateCharacteristic(this.platform.Characteristic.On, isCleaningBoundary);
      } else {
        const pendingState = this.pendingBoundaryStates.get(boundaryId);
        if (pendingState !== null && pendingState !== undefined) {
          this.pendingBoundaryStates.set(boundaryId, null);
        }
      }
    });

    this.batteryService?.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.robot.charge ?? 0);
    this.batteryService?.updateCharacteristic(this.platform.Characteristic.ChargingState, !!this.robot.isCharging);

    if (this.nextRoom != null && this.robot.isDocked) {
      const boundaryId = this.nextRoom;
      if (!this.boundaryLabels.has(boundaryId)) {
        this.nextRoom = null;
        return;
      }
      void this.clean({ boundaryId }).then(() => {
        this.nextRoom = null;
        const boundaryName = this.boundaryLabels.get(boundaryId) ?? boundaryId;
        debug(`${this.name}: ## Starting cleaning of next room (${boundaryName})`);
      }).catch(() => {
        this.nextRoom = null;
      });
    }
  }

  private informationService(): Service {
    return (
      this.accessory.getService(this.platform.Service.AccessoryInformation)
      || this.accessory.addService(this.platform.Service.AccessoryInformation)
    );
  }

  private boundaryServiceName(boundaryName: string): string {
    const splitName = boundaryName.split(' ');
    if (splitName.length >= 2 && /[']s$/g.test(splitName[splitName.length - 2])) {
      return `${this.dict.clean} ${boundaryName}`;
    }
    return `${this.dict['clean the']} ${boundaryName}`;
  }

  private ensureUniqueBoundaryName(name: string, usedNames: Set<string>): string {
    let candidate = name;
    let counter = 2;
    while (usedNames.has(candidate)) {
      candidate = `${name} ${counter}`;
      counter += 1;
    }
    usedNames.add(candidate);
    return candidate;
  }

  private setupBoundaryServices(): void {
    const maps = (Array.isArray(this.robot.maps) ? this.robot.maps : []) as Array<{ boundaries?: KoboldBoundary[] }>;
    const usedNames = new Set<string>();

    maps.forEach(map => {
      if (!Array.isArray(map.boundaries)) {
        return;
      }

      map.boundaries.forEach(boundary => {
        if (boundary.type !== 'polygon') {
          return;
        }

        if (this.boundaryServices.has(boundary.id)) {
          return;
        }

        const displayName = this.ensureUniqueBoundaryName(boundary.name, usedNames);
        this.boundaryLabels.set(boundary.id, displayName);

        const serviceName = this.boundaryServiceName(displayName);
        const service = this.createService(
          this.platform.Service.Switch,
          serviceName,
          `cleanBoundary:${boundary.id}`,
        );
        service.getCharacteristic(this.platform.Characteristic.On)
          .onSet(value => this.setClean(value, boundary.id))
          .onGet(() => this.getClean(boundary.id));

        this.boundaryServices.set(boundary.id, service);
        this.pendingBoundaryStates.set(boundary.id, null);
      });
    });
  }

  private createService(
    serviceType: HapServiceConstructor,
    name: string,
    subtype: string,
    expose = true,
  ): Service {
    const existing = this.accessory.getServiceById(serviceType, subtype);

    if (expose) {
      if (existing) {
        this.applyConfiguredName(existing, name);
        return existing;
      }
      const service = this.accessory.addService(serviceType, name, subtype);
      this.applyConfiguredName(service, name);
      return service;
    }

    if (existing) {
      this.accessory.removeService(existing);
    }

    const service = new serviceType(name, subtype);
    this.applyConfiguredName(service, name);
    return service;
  }

  private getOrAddCharacteristic(service: Service, characteristic: HapCharacteristicConstructor) {
    const ctor = characteristic as unknown as WithUUID<typeof Characteristic>;
    if (service.testCharacteristic(ctor)) {
      return service.getCharacteristic(characteristic);
    }
    return service.addCharacteristic(characteristic);
  }

  private applyConfiguredName(service: Service, name: string): void {
    service.setCharacteristic(this.platform.Characteristic.Name, name);
    service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
    service.setCharacteristic(this.platform.Characteristic.ConfiguredName, name);
  }

  private asBool(value: CharacteristicValue): boolean {
    return value === true || value === 1;
  }

  private async getClean(boundaryId?: string): Promise<CharacteristicValue> {
    if (boundaryId) {
      const pending = this.pendingBoundaryStates.get(boundaryId);
      if (pending !== null && pending !== undefined) {
        return pending;
      }
    }

    await this.platform.updateRobot(this.robot._serial);
    const cleaning = boundaryId
      ? !!this.robot.canPause && this.robot.cleaningBoundaryId === boundaryId
      : !!this.robot.canPause;
    debug(`${this.name}: Cleaning ${boundaryId ?? 'house'} is ${cleaning ? 'ON'.brightGreen : 'OFF'.red}`);
    return cleaning;
  }

  private async setClean(value: CharacteristicValue, boundaryId?: string): Promise<void> {
    const on = this.asBool(value);
    const boundaryName = boundaryId ? this.boundaryLabels.get(boundaryId) ?? boundaryId : 'home';
    debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} Clean ${boundaryName}`);

    if (boundaryId) {
      this.pendingBoundaryStates.set(boundaryId, on);
    }

    await this.platform.updateRobot(this.robot._serial);

    if (on) {
      if (!boundaryId || this.robot.cleaningBoundaryId === boundaryId) {
        if (this.robot.canResume) {
          debug(`${this.name}: ## Resume cleaning`);
          await this.runCommand(cb => this.robot.resumeCleaning(cb));
        } else if (this.robot.canStart) {
          debug(`${this.name}: ## Start cleaning`);
          await this.clean({ boundaryId });
        } else {
          debug(`${this.name}: Cannot start, maybe already cleaning (expected)`);
        }
      } else if (this.robot.canPause || this.robot.canResume) {
        debug(`${this.name}: ## Returning to dock to start cleaning of new room`);
        await this.goToDockSequence();
        this.nextRoom = boundaryId;
      } else {
        debug(`${this.name}: ## Start cleaning of new room`);
        await this.clean({ boundaryId });
      }
    } else if (this.robot.canPause) {
      debug(`${this.name}: ## Pause cleaning`);
      await this.runCommand(cb => this.robot.pauseCleaning(cb));
    } else {
      debug(`${this.name}: Already paused`);
    }

    await this.platform.updateRobot(this.robot._serial);
    if (boundaryId) {
      this.pendingBoundaryStates.set(boundaryId, null);
    }
  }

  private async clean(options?: { spot?: SpotSettings; boundaryId?: string }): Promise<void> {
    if (this.refreshSetting === 'auto') {
      setTimeout(() => {
        this.platform.updateRobotTimer(this.robot._serial);
      }, 60 * 1000);
    }

    const boundaryId = options?.boundaryId ?? null;
    const spot = options?.spot;
    const eco = !!this.robot.eco;
    const extraCare = this.robot.navigationMode === 2;
    const noGoLines = !!this.robot.noGoLines;
    const room = boundaryId ? this.boundaryLabels.get(boundaryId) ?? '' : '';

    const roomLabel = room !== '' ? `${room} ` : '';
    const detailText = `eco: ${eco}, extraCare: ${extraCare}, nogoLines: ${noGoLines}, spot: ${JSON.stringify(spot)}`;
    debug(`${this.name}: ## Start cleaning (${roomLabel}${detailText})`);

    if (!boundaryId && !spot) {
      await this.runCommand((cb) => this.robot.startCleaning(eco, extraCare ? 2 : 1, noGoLines, cb), (error, result) => {
        this.platform.log.error(`Cannot start cleaning. ${error}: ${JSON.stringify(result)}`);
      });
    } else if (boundaryId) {
      await this.runCommand((cb) => this.robot.startCleaningBoundary(eco, extraCare, boundaryId, cb), (error, result) => {
        this.platform.log.error(`Cannot start room cleaning. ${error}: ${JSON.stringify(result)}`);
      });
    } else if (spot) {
      const widthValue = spot.width ?? this.robot.spotWidth ?? this.spotWidthCharacteristic?.props.minValue ?? 100;
      const heightValue = spot.height ?? this.robot.spotHeight ?? this.spotHeightCharacteristic?.props.minValue ?? 100;
      await this.runCommand((cb) =>
        this.robot.startSpotCleaning(
          eco,
          widthValue,
          heightValue,
          spot.repeat,
          extraCare ? 2 : 1,
          cb,
        ), (error, result) => {
        this.platform.log.error(`Cannot start spot cleaning. ${error}: ${JSON.stringify(result)}`);
      });
    }
  }

  private async getGoToDock(): Promise<CharacteristicValue> {
    return false;
  }

  private async setGoToDock(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);
    if (!on) {
      return;
    }

    await this.platform.updateRobot(this.robot._serial);
    await this.goToDockSequence();
  }

  private async getEco(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const eco = !!this.robot.eco;
    debug(`${this.name}: Eco Mode is ${eco ? 'ON'.brightGreen : 'OFF'.red}`);
    return eco;
  }

  private async setEco(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);
    this.robot.eco = on;
    debug(`${this.name}: ${on ? 'Enabled '.red : 'Disabled'.red} Eco Mode`);
  }

  private async getNoGoLines(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const noGo = !!this.robot.noGoLines;
    debug(`${this.name}: NoGoLine is ${noGo ? 'ON'.brightGreen : 'OFF'.red}`);
    return noGo ? 1 : 0;
  }

  private async setNoGoLines(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);
    this.robot.noGoLines = on;
    debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} NoGoLine`);
  }

  private async getExtraCare(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    debug(`${this.name}: Care Nav is ${this.robot.navigationMode === 2 ? 'ON'.brightGreen : 'OFF'.red}`);
    return this.robot.navigationMode === 2 ? 1 : 0;
  }

  private async setExtraCare(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);
    this.robot.navigationMode = on ? 2 : 1;
    debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} Care Nav`);
  }

  private async getSchedule(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const enabled = !!this.robot.isScheduleEnabled;
    debug(`${this.name}: Schedule is ${enabled ? 'ON'.brightGreen : 'OFF'.red}`);
    return enabled;
  }

  private async setSchedule(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);
    await this.platform.updateRobot(this.robot._serial);

    if (on) {
      debug(this.name + ': ' + 'Enabled'.brightGreen + ' Schedule');
      await this.runCommand(cb => this.robot.enableSchedule(cb));
    } else {
      debug(this.name + ': ' + 'Disabled'.red + ' Schedule');
      await this.runCommand(cb => this.robot.disableSchedule(cb));
    }
  }

  private async getFindMe(): Promise<CharacteristicValue> {
    return false;
  }

  private async setFindMe(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);
    if (!on) {
      return;
    }

    debug(`${this.name}: ## Find me`);
    setTimeout(() => {
      this.findMeService?.updateCharacteristic(this.platform.Characteristic.On, false);
    }, 1000);

    await this.runCommand(cb => this.robot.findMe(cb));
  }

  private async getSpotClean(): Promise<CharacteristicValue> {
    const characteristic = this.spotCleanService?.getCharacteristic(this.platform.Characteristic.On);
    const spotService = characteristic?.value as boolean | undefined;
    return spotService ?? false;
  }

  private async setSpotClean(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);

    const spot: SpotSettings = {
      width: this.spotPlusFeatures && this.spotWidthCharacteristic
        ? ((this.spotWidthCharacteristic.value as number | undefined) ?? this.robot.spotWidth ?? this.spotWidthCharacteristic.props.minValue ?? 100)
        : this.robot.spotWidth ?? null,
      height: this.spotPlusFeatures && this.spotHeightCharacteristic
        ? ((this.spotHeightCharacteristic.value as number | undefined) ?? this.robot.spotHeight ?? this.spotHeightCharacteristic.props.minValue ?? 100)
        : this.robot.spotHeight ?? null,
      repeat: !!(this.spotRepeatCharacteristic?.value ?? false),
    };

    await this.platform.updateRobot(this.robot._serial);

    if (on) {
      if (this.robot.canResume) {
        debug(`${this.name}: ## Resume (spot) cleaning`);
        await this.runCommand(cb => this.robot.resumeCleaning(cb));
      } else if (this.robot.canStart) {
        await this.clean({ spot });
      } else {
        debug(`${this.name}: Cannot start spot cleaning, maybe already cleaning`);
      }
    } else if (this.robot.canPause) {
      debug(`${this.name}: ## Pause cleaning`);
      await this.runCommand(cb => this.robot.pauseCleaning(cb));
    } else {
      debug(`${this.name}: Already paused`);
    }
  }

  private async getSpotWidth(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const width = this.robot.spotWidth ?? this.spotWidthCharacteristic?.props.minValue ?? 100;
    debug(`${this.name}: Spot width  is ${width}cm`);
    return width;
  }

  private async setSpotWidth(value: CharacteristicValue): Promise<void> {
    const numericValue = typeof value === 'number' ? value : Number(value);
    this.robot.spotWidth = Number.isNaN(numericValue) ? this.robot.spotWidth ?? 100 : numericValue;
    debug(`${this.name}: Set spot width to ${this.robot.spotWidth}cm`);
  }

  private async getSpotHeight(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const height = this.robot.spotHeight ?? this.spotHeightCharacteristic?.props.minValue ?? 100;
    debug(`${this.name}: Spot height is ${height}cm`);
    return height;
  }

  private async setSpotHeight(value: CharacteristicValue): Promise<void> {
    const numericValue = typeof value === 'number' ? value : Number(value);
    this.robot.spotHeight = Number.isNaN(numericValue) ? this.robot.spotHeight ?? 100 : numericValue;
    debug(`${this.name}: Set spot height to ${this.robot.spotHeight}cm`);
  }

  private async getSpotRepeat(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const repeat = this.robot.spotRepeat ?? false;
    debug(`${this.name}: Spot repeat is ${repeat ? 'ON'.brightGreen : 'OFF'.red}`);
    return repeat;
  }

  private async setSpotRepeat(value: CharacteristicValue): Promise<void> {
    const on = this.asBool(value);
    this.robot.spotRepeat = on;
    debug(`${this.name}: ${on ? 'Enabled '.brightGreen : 'Disabled'.red} Spot repeat`);
  }

  private async getDock(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const isDocked = !!this.robot.isDocked;
    debug(`${this.name}: The Dock is ${isDocked ? 'OCCUPIED'.brightGreen : 'NOT OCCUPIED'.red}`);
    return isDocked ? 1 : 0;
  }

  private async getBatteryLevel(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const charge = this.robot.charge ?? 0;
    debug(`${this.name}: Battery  is ${charge}%`);
    return charge;
  }

  private async getBatteryChargingState(): Promise<CharacteristicValue> {
    await this.platform.updateRobot(this.robot._serial);
    const isCharging = !!this.robot.isCharging;
    debug(`${this.name}: Battery  is ${isCharging ? 'CHARGING'.brightGreen : 'NOT CHARGING'.red}`);
    return isCharging;
  }

  private async goToDockSequence(): Promise<void> {
    if (this.robot.canPause) {
      debug(`${this.name}: ## Pause cleaning to go to dock`);
      await this.runCommand(cb => this.robot.pauseCleaning(cb));
      await this.delay(1000);
      debug(`${this.name}: ## Go to dock`);
      await this.runCommand(cb => this.robot.sendToBase(cb));
    } else if (this.robot.canGoToBase) {
      debug(`${this.name}: ## Go to dock`);
      await this.runCommand(cb => this.robot.sendToBase(cb));
    } else {
      this.platform.log.warn(`${this.name}: Can't go to dock at the moment`);
    }
  }

  private async runCommand<T = unknown>(
    command: (callback: (error?: unknown, result?: T) => void) => void,
    onError?: (error: unknown, result?: T) => void,
  ): Promise<T | undefined> {
    return new Promise((resolve, reject) => {
      command((error?: unknown, result?: T) => {
        if (error) {
          onError?.(error, result);
          reject(error instanceof Error ? error : new Error(String(error)));
        } else {
          resolve(result);
        }
      });
    });
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}
