import {
  API,
  APIEvent,
  CharacteristicEventTypes,
  CharacteristicGetCallback,
  CharacteristicSetCallback,
  CharacteristicValue,
  DynamicPlatformPlugin,
  HAP,
  Logging,
  PlatformAccessory,
  PlatformAccessoryEvent,
  PlatformConfig,
} from 'homebridge';
import { Vehicle } from './fordpass';
import { Command } from './types/vehicle';
import { FordpassConfig, VehicleConfig } from './types/config';
import { Connection } from './fordpass-connection';
import { FordpassAccessory } from './accessory';

let hap: HAP;
let Accessory: typeof PlatformAccessory;

const PLUGIN_NAME = 'homebridge-fordpass';
const PLATFORM_NAME = 'FordPass';

class FordPassPlatform implements DynamicPlatformPlugin {
  private readonly log: Logging;
  private readonly api: API;
  private readonly accessories: Array<PlatformAccessory> = [];
  private readonly vehicles: Array<Vehicle> = [];
  private config: FordpassConfig;
  private pendingLockUpdate = false;

  constructor(log: Logging, config: PlatformConfig, api: API) {
    this.log = log;
    this.api = api;
    this.config = config as FordpassConfig;

    // Need a config or plugin will not start
    if (!config) {
      return;
    }

    if (!config.username || !config.password) {
      this.log.error('Please add a userame and password to your config.json');
      return;
    }

    api.on(APIEvent.DID_FINISH_LAUNCHING, this.didFinishLaunching.bind(this));
  }

  configureAccessory(accessory: PlatformAccessory): void {
    const self = this;
    this.log.info(`Configuring accessory ${accessory.displayName}`);

    accessory.on(PlatformAccessoryEvent.IDENTIFY, () => {
      this.log.info(`${accessory.displayName} identified!`);
    });

    const vehicle = new Vehicle(accessory.context.name, accessory.context.vin, this.config, this.log);
    const fordAccessory = new FordpassAccessory(accessory);

    // Create Lock service
    const defaultState = hap.Characteristic.LockTargetState.UNSECURED;
    const lockService = fordAccessory.createService(hap.Service.LockMechanism);
    const switchService = fordAccessory.createService(hap.Service.Switch);
    const batteryService = fordAccessory.createService(
      hap.Service.Battery,
      this.config.options?.batteryName || 'Fuel Level',
    );
    lockService.setCharacteristic(hap.Characteristic.LockCurrentState, defaultState);

    lockService
      .setCharacteristic(hap.Characteristic.LockTargetState, defaultState)
      .getCharacteristic(hap.Characteristic.LockTargetState)
      .on(CharacteristicEventTypes.SET, async (value: CharacteristicValue, callback: CharacteristicSetCallback) => {
        this.log.debug(`${value ? 'Locking' : 'Unlocking'} ${accessory.displayName}`);
        let commandId = '';
        let command = Command.LOCK;
        if (value === hap.Characteristic.LockTargetState.UNSECURED) {
          command = Command.UNLOCK;
        }
        commandId = await vehicle.issueCommand(command);

        let tries = 30;
        this.pendingLockUpdate = true;
        const self = this;
        const interval = setInterval(async () => {
          if (tries > 0) {
            const status = await vehicle.commandStatus(command, commandId);
            if (status?.status === 200) {
              lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, value);
              self.pendingLockUpdate = false;
              clearInterval(interval);
            }
            tries--;
          } else {
            self.pendingLockUpdate = false;
            clearInterval(interval);
          }
        }, 1000);
        callback(undefined, value);
      })
      .on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => {
        // Return cached value immediately then update properly
        let lockNumber = hap.Characteristic.LockTargetState.UNSECURED;
        const lockStatus = vehicle?.info?.lockStatus.value;
        if (lockStatus === 'LOCKED') {
          lockNumber = hap.Characteristic.LockTargetState.SECURED;
        }
        callback(undefined, lockNumber);

        const status = await vehicle.status();
        if (status) {
          let lockNumber = hap.Characteristic.LockTargetState.UNSECURED;
          const lockStatus = status.lockStatus.value;
          if (lockStatus === 'LOCKED') {
            lockNumber = hap.Characteristic.LockTargetState.SECURED;
          }
          lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber);
          lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber);
        } else {
          self.log.error(`Cannot get information for ${accessory.displayName} lock`);
        }
      });

    switchService
      .setCharacteristic(hap.Characteristic.On, false)
      .getCharacteristic(hap.Characteristic.On)
      .on(CharacteristicEventTypes.SET, async (value: CharacteristicValue, callback: CharacteristicSetCallback) => {
        this.log.debug(`${value ? 'Starting' : 'Stopping'} ${accessory.displayName}`);
        if (value as boolean) {
          await vehicle.issueCommand(Command.START);
        } else {
          await vehicle.issueCommand(Command.STOP);
        }
        callback(undefined, value);
      })
      .on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => {
        // Return cached value immediately then update properly
        const engineStatus = vehicle?.info?.remoteStartStatus.value || 0;
        callback(undefined, engineStatus);
        const status = await vehicle.status();
        if (status) {
          let started = false;
          const engineStatus = status.remoteStartStatus.value || 0;
          if (engineStatus > 0) {
            started = true;
          }
          switchService.updateCharacteristic(hap.Characteristic.On, started);
        } else {
          self.log.error(`Cannot get information for ${accessory.displayName} engine`);
        }
      });

    batteryService
      .setCharacteristic(hap.Characteristic.BatteryLevel, 100)
      .getCharacteristic(hap.Characteristic.BatteryLevel)
      .on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => {
        // Return cached value immediately then update properly
        const fuel = vehicle?.info?.fuel?.fuelLevel;
        const battery = vehicle?.info?.batteryFillLevel?.value;
        let level = fuel || battery || 100;
        if (level > 100) {
          level = 100;
        }
        if (level < 0) {
          level = 0;
        }
        callback(undefined, level);
        const status = await vehicle.status();
        if (status) {
          const fuel = status.fuel?.fuelLevel;
          const battery = status.batteryFillLevel?.value;
          const chargingStatus = vehicle?.info?.chargingStatus?.value;
          let level = fuel || battery || 100;
          if (level > 100) {
            level = 100;
          }
          if (level < 0) {
            level = 0;
          }
          batteryService.updateCharacteristic(hap.Characteristic.BatteryLevel, level);
          if (battery) {
            if (chargingStatus === 'ChargingAC') {
              batteryService.updateCharacteristic(
                hap.Characteristic.ChargingState,
                hap.Characteristic.ChargingState.CHARGING,
              );
            } else {
              batteryService.updateCharacteristic(
                hap.Characteristic.ChargingState,
                hap.Characteristic.ChargingState.NOT_CHARGING,
              );
            }
          } else {
            batteryService.updateCharacteristic(
              hap.Characteristic.ChargingState,
              hap.Characteristic.ChargingState.NOT_CHARGEABLE,
            );
          }

          if (level < 10) {
            batteryService.updateCharacteristic(
              hap.Characteristic.StatusLowBattery,
              hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW,
            );
          } else {
            batteryService.updateCharacteristic(
              hap.Characteristic.StatusLowBattery,
              hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL,
            );
          }
        } else {
          self.log.error(`Cannot get information for ${accessory.displayName} engine`);
        }
      });
    this.vehicles.push(vehicle);
    this.accessories.push(accessory);
  }

  async didFinishLaunching(): Promise<void> {
    const self = this;
    const ford = new Connection(this.config, this.log);
    const authInfo = await ford.auth();

    if (authInfo) {
      setInterval(async () => {
        self.log.debug('Reauthenticating with refresh token');
        await ford.refreshAuth();
      }, authInfo.expires_in * 1000 - 10000);

      await this.addVehicles(ford);
      await this.updateVehicles();
      await this.refreshVehicles();

      // Vehicle info needs to be updated every minute
      setInterval(async () => {
        await self.updateVehicles();
      }, 60 * 1000);
    }
  }

  async addVehicles(connection: Connection): Promise<void> {
    const vehicles = await connection.getVehicles();
    vehicles?.forEach(async (vehicle: VehicleConfig) => {
      vehicle.vin = vehicle.vin.toUpperCase();
      const name = vehicle.nickName || vehicle.vehicleType;
      const uuid = hap.uuid.generate(vehicle.vin);
      const accessory = new Accessory(name, uuid);
      accessory.context.name = name;
      accessory.context.vin = vehicle.vin;

      const accessoryInformation = accessory.getService(hap.Service.AccessoryInformation);
      if (accessoryInformation) {
        accessoryInformation.setCharacteristic(hap.Characteristic.Manufacturer, 'Ford');
        accessoryInformation.setCharacteristic(hap.Characteristic.Model, name);
        accessoryInformation.setCharacteristic(hap.Characteristic.SerialNumber, vehicle.vin);
      }

      // Only add new cameras that are not cached
      if (!this.accessories.find((x: PlatformAccessory) => x.UUID === uuid)) {
        this.log.debug(`New vehicle found: ${name}`);
        this.configureAccessory(accessory); // abusing the configureAccessory here
        this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
      }
    });

    // Remove vehicles that were removed from config
    this.accessories.forEach((accessory: PlatformAccessory<Record<string, string>>) => {
      if (!vehicles?.find((x: VehicleConfig) => x.vin === accessory.context.vin)) {
        this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
        const index = this.accessories.indexOf(accessory);
        if (index > -1) {
          this.accessories.splice(index, 1);
          this.vehicles.slice(index, 1);
        }
      }
    });
  }

  async updateVehicles(): Promise<void> {
    this.vehicles.forEach(async (vehicle: Vehicle) => {
      const status = await vehicle.status();
      this.log.debug(`Updating info for ${vehicle.name}`);
      const lockStatus = status?.lockStatus.value;
      let lockNumber = hap.Characteristic.LockCurrentState.UNSECURED;
      if (lockStatus === 'LOCKED') {
        lockNumber = hap.Characteristic.LockCurrentState.SECURED;
      }

      const engineStatus = status?.remoteStartStatus.value || 0;
      let started = false;
      if (engineStatus > 0) {
        started = true;
      }
      const uuid = hap.uuid.generate(vehicle.vin);
      const accessory = this.accessories.find((x: PlatformAccessory) => x.UUID === uuid);

      if (!this.pendingLockUpdate) {
        const lockService = accessory?.getService(hap.Service.LockMechanism);
        lockService && lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber);
        lockService && lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber);
      }

      const switchService = accessory?.getService(hap.Service.Switch);
      switchService && switchService.updateCharacteristic(hap.Characteristic.On, started);
    });
  }

  async refreshVehicles(): Promise<void> {
    this.vehicles.forEach(async (vehicle: Vehicle) => {
      if (vehicle.autoRefresh && vehicle.refreshRate && vehicle.refreshRate > 0) {
        this.log.debug(`Configuring ${vehicle.name} to refresh every ${vehicle.refreshRate} minutes.`);
        setInterval(async () => {
          this.log.debug(`Refreshing info for ${vehicle.name}`);
          await vehicle.issueCommand(Command.REFRESH);
        }, 60000 * vehicle.refreshRate);
      }
    });
  }
}

export = (api: API): void => {
  hap = api.hap;
  Accessory = api.platformAccessory;

  api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, FordPassPlatform);
};