import { CharacteristicEventTypes } from 'homebridge'
import type {
  Service,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  Characteristic as HomebridgeCharacteristic,
  CharacteristicValue,
  CharacteristicGetCallback,
  CharacteristicSetCallback,
  WithUUID,
} from 'homebridge'

import { HomebridgeLgThinqPlatform } from '../platform'
import type { GetDeviceResponse } from '../thinq/apiTypes'

export default abstract class AbstractCharacteristic<
  State extends CharacteristicValue,
  ApiValue extends string | number,
  Characteristic extends WithUUID<{
    new (): HomebridgeCharacteristic
  }> /** Comes from this.platform.Characteristic.____ */,
> {
  private platform: HomebridgeLgThinqPlatform
  private service: Service
  private deviceId: string
  private cachedState?: State
  characteristic: Characteristic /** Comes from this.platform.Characteristic.____ */
  private apiCommand: 'Set' | 'Operation'
  private apiDataKey: keyof GetDeviceResponse['result']['snapshot']

  get thinqApi() {
    return this.platform.thinqApi
  }

  constructor(
    platform: HomebridgeLgThinqPlatform,
    service: Service,
    deviceId: string,
    characteristic: Characteristic,
    apiCommand: 'Set' | 'Operation',
    apiDataKey: keyof GetDeviceResponse['result']['snapshot'],
  ) {
    this.platform = platform
    this.service = service
    this.deviceId = deviceId
    this.characteristic = characteristic
    this.apiCommand = apiCommand
    this.apiDataKey = apiDataKey

    if (this.handleSet) {
      // read-only characteristics won't have a handleSet
      this.service
        .getCharacteristic(this.characteristic)
        .on(CharacteristicEventTypes.SET, this.handleSet.bind(this))
    }
  }

  /** Transform Homebridge state to what the ThinQ API expects */
  abstract getStateFromApiValue(apiValue: ApiValue): State

  /** Transform the value from the ThinQ API to Homebridge state.
   * NOTE: This should make use of this.characteristic.____ enum values
   */
  abstract getApiValueFromState(state: State): ApiValue

  /** Take in an updated device snapshot */
  handleUpdatedSnapshot(snapshot: GetDeviceResponse['result']['snapshot']) {
    try {
      const apiValue = snapshot[this.apiDataKey] as ApiValue
      this.logDebug('handleUpdatedSnapshot', apiValue)
      this.cachedState = this.getStateFromApiValue(apiValue)
      this.service.updateCharacteristic(this.characteristic, this.cachedState)
    } catch (error) {
      this.logError('Error parsing state', `${error}`)
    }
  }

  /** Handle a "set" command from Homebridge to update this characteristic */
  handleSet?(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    this.logDebug('Triggered SET:', value)
    if (!this.thinqApi) {
      this.logError('API not initialized yet')
      return
    }

    // Double-transform the value
    const targetState = this.getStateFromApiValue(
      this.getApiValueFromState(value as State),
    )
    this.logDebug('targetState', targetState)

    if (targetState === this.cachedState) {
      // The air conditioner will make a sound every time this API is called.
      // To avoid unnecessary chimes, we'll optimistically skip sending the API call.
      this.logDebug('State equals cached state. Skipping.', targetState)
      callback(null)
      return
    }

    const apiValue = this.getApiValueFromState(targetState)

    this.thinqApi
      .sendCommand(this.deviceId, this.apiCommand, this.apiDataKey, apiValue)
      .then(() => {
        this.cachedState = targetState
        callback(null)
      })
      .catch((error) => {
        this.logError('Failed to set state', targetState, `${error}`)
        callback(error)
      })
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  logError(message: string, ...parameters: any[]) {
    this.platform.log.error(
      this.constructor.name + ': ' + message,
      ...parameters,
    )
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  logDebug(message: string, ...parameters: any[]) {
    this.platform.log.debug(
      this.constructor.name + ': ' + message,
      ...parameters,
    )
  }
}