/* eslint-disable max-classes-per-file */
import type {
  HassEntity,
  HassEntityAttributeBase,
} from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
import { ClimateEntityFeature } from "../data/climate";

const now = () => new Date().toISOString();
const randomTime = () =>
  new Date(new Date().getTime() - Math.random() * 80 * 60 * 1000).toISOString();

const CAPABILITY_ATTRIBUTES = [
  "friendly_name",
  "unit_of_measurement",
  "icon",
  "entity_picture",
  "supported_features",
  "hidden",
  "assumed_state",
  "device_class",
  "state_class",
  "restored",
];
export class Entity {
  public domain: string;

  public objectId: string;

  public entityId: string;

  public lastChanged: string;

  public lastUpdated: string;

  public state: string;

  public baseAttributes: HassEntityAttributeBase & Record<string, any>;

  public attributes: HassEntityAttributeBase & Record<string, any>;

  public hass?: any;

  static CAPABILITY_ATTRIBUTES = new Set(CAPABILITY_ATTRIBUTES);

  constructor(domain, objectId, state, attributes) {
    this.domain = domain;
    this.objectId = objectId;
    this.entityId = `${domain}.${objectId}`;
    this.lastChanged = randomTime();
    this.lastUpdated = randomTime();
    this.state = String(state);

    // These are the attributes that we always write to the state machine
    const baseAttributes = {};
    const capabilityAttributes =
      TYPES[domain]?.CAPABILITY_ATTRIBUTES || Entity.CAPABILITY_ATTRIBUTES;
    for (const key of Object.keys(attributes)) {
      if (capabilityAttributes.has(key)) {
        baseAttributes[key] = attributes[key];
      }
    }

    this.baseAttributes = baseAttributes;
    this.attributes = attributes;
  }

  public async handleService(domain, service, data: Record<string, any>) {
    // eslint-disable-next-line
    console.log(
      `Unmocked service for ${this.entityId}: ${domain}/${service}`,
      data
    );
  }

  public update(state, attributes = {}) {
    this.state = state;
    this.lastUpdated = now();
    this.lastChanged =
      state === this.state ? this.lastChanged : this.lastUpdated;
    this.attributes = { ...this.attributes, ...attributes };

    // eslint-disable-next-line
    console.log("update", this.entityId, this);

    this.hass.updateStates({
      [this.entityId]: this.toState(),
    });
  }

  public toState() {
    return {
      entity_id: this.entityId,
      state: this.state,
      attributes: this.state === "off" ? this.baseAttributes : this.attributes,
      last_changed: this.lastChanged,
      last_updated: this.lastUpdated,
    };
  }
}

class LightEntity extends Entity {
  static CAPABILITY_ATTRIBUTES = new Set([
    ...CAPABILITY_ATTRIBUTES,
    "min_color_temp_kelvin",
    "max_color_temp_kelvin",
    "min_mireds",
    "max_mireds",
    "effect_list",
    "supported_color_modes",
  ]);

  public async handleService(domain, service, data) {
    if (!["homeassistant", this.domain].includes(domain)) {
      return;
    }

    if (service === "turn_on") {
      // eslint-disable-next-line
      let { hs_color, brightness_pct, rgb_color, color_temp } = data;
      const attrs = { ...this.attributes };
      if (brightness_pct) {
        attrs.brightness = (255 * brightness_pct) / 100;
      } else if (!attrs.brightness) {
        attrs.brightness = 255;
      }
      if (hs_color) {
        attrs.color_mode = "hs";
        attrs.hs_color = hs_color;
      }
      if (rgb_color) {
        attrs.color_mode = "rgb";
        attrs.rgb_color = rgb_color;
      }
      if (color_temp) {
        attrs.color_mode = "color_temp";
        attrs.color_temp = color_temp;
        delete attrs.rgb_color;
      }
      this.update("on", attrs);
    } else if (service === "turn_off") {
      this.update("off");
    } else if (service === "toggle") {
      if (this.state === "on") {
        this.handleService(domain, "turn_off", data);
      } else {
        this.handleService(domain, "turn_on", data);
      }
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class ToggleEntity extends Entity {
  public async handleService(domain, service, data) {
    if (!["homeassistant", this.domain].includes(domain)) {
      return;
    }

    if (service === "turn_on") {
      this.update("on", this.attributes);
    } else if (service === "turn_off") {
      this.update("off", this.attributes);
    } else if (service === "toggle") {
      if (this.state === "on") {
        this.handleService(domain, "turn_off", data);
      } else {
        this.handleService(domain, "turn_on", data);
      }
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class LockEntity extends Entity {
  public async handleService(
    domain,
    service,
    // @ts-ignore
    data
  ) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "lock") {
      this.update("locked");
    } else if (service === "unlock") {
      this.update("unlocked");
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class AlarmControlPanelEntity extends Entity {
  public async handleService(
    domain,
    service,
    // @ts-ignore
    data
  ) {
    if (domain !== this.domain) {
      return;
    }

    const serviceStateMap = {
      alarm_arm_night: "armed_night",
      alarm_arm_home: "armed_home",
      alarm_arm_away: "armed_away",
      alarm_disarm: "disarmed",
    };

    if (serviceStateMap[service]) {
      this.update(serviceStateMap[service], this.baseAttributes);
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class MediaPlayerEntity extends Entity {
  static CAPABILITY_ATTRIBUTES = new Set([
    ...CAPABILITY_ATTRIBUTES,
    "source_list",
    "sound_mode_list",
  ]);

  public async handleService(
    domain,
    service,
    // @ts-ignore
    data
  ) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "media_play_pause") {
      this.update(
        this.state === "playing" ? "paused" : "playing",
        this.attributes
      );
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class CoverEntity extends Entity {
  public async handleService(
    domain,
    service,
    // @ts-ignore
    data
  ) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "open_cover") {
      this.update("open");
    } else if (service === "close_cover") {
      this.update("closed");
    } else if (service === "set_cover_position") {
      this.update(data.position > 0 ? "open" : "closed", {
        current_position: data.position,
      });
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class InputNumberEntity extends Entity {
  public async handleService(
    domain,
    service,
    // @ts-ignore
    data
  ) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "set_value") {
      this.update("" + data.value);
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class InputTextEntity extends Entity {
  public async handleService(
    domain,
    service,
    // @ts-ignore
    data
  ) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "set_value") {
      this.update("" + data.value);
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class InputSelectEntity extends Entity {
  public async handleService(
    domain,
    service,
    // @ts-ignore
    data
  ) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "select_option") {
      this.update("" + data.option);
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class ClimateEntity extends Entity {
  static CAPABILITY_ATTRIBUTES = new Set([
    ...CAPABILITY_ATTRIBUTES,
    "hvac_modes",
    "min_temp",
    "max_temp",
    "target_temp_step",
    "fan_modes",
    "preset_modes",
    "swing_modes",
    "min_humidity",
    "max_humidity",
  ]);

  public async handleService(domain, service, data) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "set_hvac_mode") {
      this.update(data.hvac_mode, this.attributes);
    } else if (
      [
        "set_temperature",
        "set_humidity",
        "set_hvac_mode",
        "set_fan_mode",
        "set_preset_mode",
        "set_swing_mode",
        "set_aux_heat",
      ].includes(service)
    ) {
      const { entity_id, ...toSet } = data;
      this.update(this.state, {
        ...this.attributes,
        ...toSet,
      });
    } else {
      super.handleService(domain, service, data);
    }
  }

  public toState() {
    const state = super.toState();

    state.attributes.hvac_action = undefined;

    if (
      supportsFeature(
        state as HassEntity,
        ClimateEntityFeature.TARGET_TEMPERATURE
      )
    ) {
      const current = state.attributes.current_temperature;
      const target = state.attributes.temperature;
      if (state.state === "heat") {
        state.attributes.hvac_action = target >= current ? "heating" : "idle";
      }
      if (state.state === "cool") {
        state.attributes.hvac_action = target <= current ? "cooling" : "idle";
      }
    }
    if (
      supportsFeature(
        state as HassEntity,
        ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
      )
    ) {
      const current = state.attributes.current_temperature;
      const lowTarget = state.attributes.target_temp_low;
      const highTarget = state.attributes.target_temp_high;
      state.attributes.hvac_action =
        lowTarget >= current
          ? "heating"
          : highTarget <= current
            ? "cooling"
            : "idle";
    }
    return state;
  }
}

class WaterHeaterEntity extends Entity {
  static CAPABILITY_ATTRIBUTES = new Set([
    ...CAPABILITY_ATTRIBUTES,
    "current_temperature",
    "min_temp",
    "max_temp",
    "operation_list",
  ]);

  public async handleService(domain, service, data) {
    if (domain !== this.domain) {
      return;
    }

    if (service === "set_operation_mode") {
      this.update(data.operation_mode, this.attributes);
    } else if (["set_temperature"].includes(service)) {
      const { entity_id, ...toSet } = data;
      this.update(this.state, {
        ...this.attributes,
        ...toSet,
      });
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class FanEntity extends Entity {
  static CAPABILITY_ATTRIBUTES = new Set([
    ...CAPABILITY_ATTRIBUTES,
    "direction",
    "oscillating",
    "percentage",
  ]);

  public async handleService(domain, service, data) {
    if (domain !== this.domain) {
      return;
    }

    if (["turn_on", "turn_off"].includes(service)) {
      this.update(service === "turn_on" ? "on" : "off");
    } else if (
      ["set_direction", "oscillate", "set_percentage"].includes(service)
    ) {
      const { entity_id, ...toSet } = data;
      this.update(this.state, {
        ...this.attributes,
        ...toSet,
      });
    } else {
      super.handleService(domain, service, data);
    }
  }
}

class GroupEntity extends Entity {
  public async handleService(domain, service, data) {
    if (!["homeassistant", this.domain].includes(domain)) {
      return;
    }

    await Promise.all(
      this.attributes.entity_id.map((ent) => {
        const entity = this.hass.mockEntities[ent];
        return entity.handleService(entity.domain, service, data);
      })
    );

    this.update(service === "turn_on" ? "on" : "off");
  }
}

const TYPES = {
  automation: ToggleEntity,
  alarm_control_panel: AlarmControlPanelEntity,
  climate: ClimateEntity,
  cover: CoverEntity,
  fan: FanEntity,
  group: GroupEntity,
  input_boolean: ToggleEntity,
  input_number: InputNumberEntity,
  input_text: InputTextEntity,
  input_select: InputSelectEntity,
  light: LightEntity,
  lock: LockEntity,
  media_player: MediaPlayerEntity,
  switch: ToggleEntity,
  water_heater: WaterHeaterEntity,
};

export const getEntity = (
  domain,
  objectId,
  state,
  baseAttributes = {}
): Entity =>
  new (TYPES[domain] || Entity)(domain, objectId, state, baseAttributes);

type LimitedEntity = Pick<HassEntity, "state" | "attributes" | "entity_id">;

export const convertEntities = (
  states: Record<string, LimitedEntity>
): Entity[] =>
  Object.keys(states).map((entId) => {
    const stateObj = states[entId];
    const [domain, objectId] = entId.split(".", 2);
    return getEntity(domain, objectId, stateObj.state, stateObj.attributes);
  });
