import {
  harmonyDefaultDelaySecs,
  harmonyDefaultHoldSecs,
  harmonyDefaultRepeats,
  harmonyDisplayName,
  harmonyHubPort,
  harmonyManufacturer,
  harmonyPowerOffActivity,
  harmonyPreviousActiveActivity,
  harmonySelectPowerOffOption,
} from './harmony.constants.js';
import type {
  IHarmonyActivity,
  IHarmonyCommand,
  IHarmonyCommandContext,
  IHarmonyConfig,
  IHarmonyDevice,
  IHarmonyDeviceCommand,
  IHarmonyDeviceCommandGroup,
  IHarmonyDeviceInfo,
  IHarmonyRemoteCommandStep,
  IHarmonySnapshot,
  THarmonyJsonValue,
} from './harmony.types.js';

export class HarmonyUnsupportedProtocolError extends Error {
  constructor(commandArg: IHarmonyCommand) {
    super(`Harmony Hub command "${commandArg.action}" requires an injected client/executor. This TypeScript port models local Harmony Hub state and commands but does not implement live aioharmony/websocket transport.`);
    this.name = 'HarmonyUnsupportedProtocolError';
  }
}

export class HarmonyUnsupportedCloudOperationError extends Error {
  constructor() {
    super('Harmony Hub sync is cloud-backed and is not implemented by this local-first TypeScript port.');
    this.name = 'HarmonyUnsupportedCloudOperationError';
  }
}

export class HarmonyClient {
  private snapshot?: IHarmonySnapshot;

  constructor(private readonly config: IHarmonyConfig) {
    this.snapshot = config.snapshot ? this.cloneSnapshot(config.snapshot) : undefined;
  }

  public get defaultActivity(): string | undefined {
    return this.stringValue(this.config.defaultActivity || this.config.activity);
  }

  public get delaySecs(): number {
    return this.positiveNumber(this.config.delaySecs ?? this.config.delay_secs) ?? harmonyDefaultDelaySecs;
  }

  public get holdSecs(): number {
    return this.nonNegativeNumber(this.config.holdSecs ?? this.config.hold_secs) ?? harmonyDefaultHoldSecs;
  }

  public get numRepeats(): number {
    return this.repeats(this.config.numRepeats ?? this.config.num_repeats);
  }

  public async getSnapshot(): Promise<IHarmonySnapshot> {
    if (this.config.client?.getSnapshot) {
      return this.normalizeSnapshot(await this.config.client.getSnapshot());
    }
    this.snapshot = this.normalizeSnapshot(this.snapshot || this.snapshotFromManualConfig());
    return this.cloneSnapshot(this.snapshot);
  }

  public async connect(): Promise<void> {
    await this.execute({ action: 'connect', reason: 'connect' });
  }

  public async startActivity(activityArg: string, reasonArg: IHarmonyCommand['reason'] = 'remote_turn_on'): Promise<void> {
    const snapshot = await this.getSnapshot();
    const activity = this.resolveActivity(snapshot, activityArg);
    if (!activity) {
      throw new Error(`Harmony Hub activity is not configured: ${activityArg}`);
    }
    if (snapshot.state.currentActivity === activity.label || snapshot.state.currentActivityId === activity.id) {
      return;
    }
    await this.execute({
      action: activity.isPowerOff ? 'power_off' : 'start_activity',
      reason: reasonArg,
      activityId: activity.id,
      activity: activity.label,
    });
  }

  public async powerOff(reasonArg: IHarmonyCommand['reason'] = 'remote_turn_off'): Promise<void> {
    await this.execute({ action: 'power_off', reason: reasonArg, activityId: '-1', activity: harmonyPowerOffActivity });
  }

  public async sendCommands(
    commandsArg: string[],
    deviceArg: string,
    optionsArg: {
      numRepeats?: number;
      delaySecs?: number;
      holdSecs?: number;
      reason?: IHarmonyCommand['reason'];
    } = {}
  ): Promise<void> {
    const snapshot = await this.getSnapshot();
    const device = this.resolveDevice(snapshot, deviceArg);
    if (!device) {
      throw new Error(`Harmony Hub device is not configured: ${deviceArg}`);
    }
    const commands = commandsArg.map((commandArg) => commandArg.trim()).filter(Boolean);
    if (!commands.length) {
      throw new Error('Harmony Hub remote.send_command requires at least one command.');
    }
    const numRepeats = this.repeats(optionsArg.numRepeats);
    const holdSecs = this.nonNegativeNumber(optionsArg.holdSecs) ?? this.holdSecs;
    const sequence: IHarmonyRemoteCommandStep[] = [];
    for (let repeatIndex = 0; repeatIndex < numRepeats; repeatIndex += 1) {
      for (const command of commands) {
        sequence.push({ deviceId: device.id, device: device.label, command, holdSecs });
      }
    }
    await this.execute({
      action: 'send_command',
      reason: optionsArg.reason || 'remote_send_command',
      deviceId: device.id,
      device: device.label,
      commands,
      sequence,
      numRepeats,
      delaySecs: this.nonNegativeNumber(optionsArg.delaySecs) ?? this.delaySecs,
      holdSecs,
    });
  }

  public async changeChannel(channelArg: number, reasonArg: IHarmonyCommand['reason'] = 'change_channel'): Promise<void> {
    if (!Number.isInteger(channelArg) || channelArg < 1) {
      throw new Error('Harmony Hub change_channel requires a positive integer channel.');
    }
    await this.execute({ action: 'change_channel', reason: reasonArg, channel: channelArg });
  }

  public async sync(): Promise<void> {
    throw new HarmonyUnsupportedCloudOperationError();
  }

  public async destroy(): Promise<void> {
    await this.config.client?.destroy?.();
  }

  private async execute(commandArg: IHarmonyCommand): Promise<void> {
    const snapshot = await this.getSnapshot();
    const context: IHarmonyCommandContext = { config: this.config, snapshot };
    if (this.config.client?.execute) {
      await this.config.client.execute(commandArg, context);
      this.applyCommand(commandArg);
      return;
    }
    if (!this.config.executor) {
      throw new HarmonyUnsupportedProtocolError(commandArg);
    }
    if (typeof this.config.executor === 'function') {
      await this.config.executor(commandArg, context);
    } else {
      await this.config.executor.execute(commandArg, context);
    }
    this.applyCommand(commandArg);
  }

  private applyCommand(commandArg: IHarmonyCommand): void {
    if (!this.snapshot) {
      return;
    }
    if (commandArg.action === 'start_activity' || commandArg.action === 'power_off') {
      const currentActivity = commandArg.activity || (commandArg.action === 'power_off' ? harmonyPowerOffActivity : undefined);
      this.snapshot = this.normalizeSnapshot({
        ...this.snapshot,
        state: {
          ...this.snapshot.state,
          available: true,
          currentActivityId: commandArg.activityId,
          currentActivity,
          lastActivity: commandArg.action === 'power_off' ? this.snapshot.state.lastActivity : currentActivity || this.snapshot.state.lastActivity,
          activityStarting: null,
        },
        updatedAt: new Date().toISOString(),
      });
    }
  }

  private snapshotFromManualConfig(): IHarmonySnapshot {
    const hubConfig = asRecord(this.config.hubConfig);
    const global = asRecord(hubConfig?.global);
    const deviceInfo: IHarmonyDeviceInfo = {
      ...this.config.deviceInfo,
      id: this.config.deviceInfo?.id || this.config.remoteId || this.config.hubId || this.stringValue(global?.timeStampHash) || this.config.host,
      name: this.config.deviceInfo?.name || this.config.deviceName || this.config.name || harmonyDisplayName,
      host: this.config.deviceInfo?.host || this.config.host,
      port: this.config.deviceInfo?.port || this.config.port || harmonyHubPort,
      remoteId: this.config.deviceInfo?.remoteId || this.config.remoteId || this.config.hubId,
      hubId: this.config.deviceInfo?.hubId || this.config.hubId || this.config.remoteId,
      manufacturer: this.config.deviceInfo?.manufacturer || this.config.manufacturer || harmonyManufacturer,
      model: this.config.deviceInfo?.model || this.config.model || 'Harmony Hub',
      softwareVersion: this.config.deviceInfo?.softwareVersion || this.config.deviceInfo?.firmwareVersion,
      timeStampHash: this.config.deviceInfo?.timeStampHash || this.stringValue(global?.timeStampHash),
    };
    const activities = this.normalizeActivities(this.config.activities || asArray(hubConfig?.activity));
    const currentActivity = this.stringValue(this.config.currentActivity || this.config.state?.currentActivity) || harmonyPowerOffActivity;
    return {
      deviceInfo,
      state: {
        available: this.config.state?.available ?? Boolean(this.config.client),
        currentActivity,
        currentActivityId: this.stringValue(this.config.currentActivityId ?? this.config.state?.currentActivityId) || this.activityIdForLabel(activities, currentActivity),
        activityStarting: this.config.state?.activityStarting ?? null,
        lastActivity: this.config.lastActivity || this.config.state?.lastActivity || null,
        rawState: this.config.state?.rawState,
      },
      activities,
      devices: this.normalizeDevices(this.config.devices || asArray(hubConfig?.device)),
      hubConfig: this.config.hubConfig as THarmonyJsonValue | undefined,
    };
  }

  private normalizeSnapshot(snapshotArg: IHarmonySnapshot): IHarmonySnapshot {
    const activities = this.normalizeActivities(snapshotArg.activities);
    const currentActivity = this.stringValue(snapshotArg.state.currentActivity) || this.activityLabelForId(activities, snapshotArg.state.currentActivityId) || harmonyPowerOffActivity;
    const deviceInfo: IHarmonyDeviceInfo = {
      ...snapshotArg.deviceInfo,
      id: snapshotArg.deviceInfo.id || snapshotArg.deviceInfo.remoteId || snapshotArg.deviceInfo.hubId || this.config.remoteId || this.config.hubId || this.config.host,
      name: snapshotArg.deviceInfo.name || this.config.name || this.config.deviceName || harmonyDisplayName,
      host: snapshotArg.deviceInfo.host || this.config.host,
      port: snapshotArg.deviceInfo.port || this.config.port || harmonyHubPort,
      manufacturer: snapshotArg.deviceInfo.manufacturer || this.config.manufacturer || harmonyManufacturer,
      model: snapshotArg.deviceInfo.model || this.config.model || 'Harmony Hub',
    };
    return {
      deviceInfo,
      state: {
        ...snapshotArg.state,
        available: snapshotArg.state.available ?? Boolean(this.config.client),
        currentActivity,
        currentActivityId: this.stringValue(snapshotArg.state.currentActivityId) || this.activityIdForLabel(activities, currentActivity),
        activityStarting: snapshotArg.state.activityStarting ?? null,
        lastActivity: snapshotArg.state.lastActivity ?? null,
      },
      activities,
      devices: this.normalizeDevices(snapshotArg.devices),
      hubConfig: snapshotArg.hubConfig,
      updatedAt: snapshotArg.updatedAt,
    };
  }

  private normalizeActivities(activitiesArg: IHarmonyConfig['activities'] | undefined): IHarmonyActivity[] {
    const activities = (activitiesArg || [])
      .map((activityArg) => this.normalizeActivity(activityArg))
      .filter((activityArg): activityArg is IHarmonyActivity => Boolean(activityArg));
    if (!activities.some((activityArg) => activityArg.isPowerOff || activityArg.label === harmonyPowerOffActivity || activityArg.id === '-1')) {
      activities.unshift({ id: '-1', label: harmonyPowerOffActivity, isPowerOff: true });
    }
    return activities;
  }

  private normalizeActivity(activityArg: IHarmonyActivity | Record<string, unknown>): IHarmonyActivity | undefined {
    const record = activityArg as Record<string, unknown>;
    const id = this.stringValue(record.id ?? record.activityId ?? record.activity_id);
    const label = this.stringValue(record.label ?? record.name ?? record.activityName ?? record.activity_name);
    if (!id && !label) {
      return undefined;
    }
    const normalizedLabel = label || (id === '-1' ? harmonyPowerOffActivity : id) || 'Activity';
    return {
      id: id || normalizedLabel,
      label: normalizedLabel,
      isPowerOff: record.isPowerOff === true || id === '-1' || normalizedLabel === harmonyPowerOffActivity,
      raw: record,
    };
  }

  private normalizeDevices(devicesArg: IHarmonyConfig['devices'] | undefined): IHarmonyDevice[] {
    return (devicesArg || [])
      .map((deviceArg) => this.normalizeDevice(deviceArg))
      .filter((deviceArg): deviceArg is IHarmonyDevice => Boolean(deviceArg));
  }

  private normalizeDevice(deviceArg: IHarmonyDevice | Record<string, unknown>): IHarmonyDevice | undefined {
    const record = deviceArg as Record<string, unknown>;
    const id = this.stringValue(record.id ?? record.deviceId ?? record.device_id);
    const label = this.stringValue(record.label ?? record.name ?? record.deviceName ?? record.device_name);
    if (!id && !label) {
      return undefined;
    }
    const normalizedLabel = label || id || 'Device';
    const commandGroups = this.normalizeCommandGroups(record.commandGroups ?? record.controlGroup ?? record.control_group);
    return {
      id: id || normalizedLabel,
      label: normalizedLabel,
      manufacturer: this.stringValue(record.manufacturer),
      model: this.stringValue(record.model),
      commandGroups,
      commands: this.flattenCommands(commandGroups),
      raw: record,
    };
  }

  private normalizeCommandGroups(valueArg: unknown): IHarmonyDeviceCommandGroup[] {
    if (!Array.isArray(valueArg)) {
      return [];
    }
    return valueArg.map((groupArg) => {
      const group = asRecord(groupArg) || {};
      return {
        name: this.stringValue(group.name) || 'Commands',
        commands: this.normalizeCommands(group.function || group.functions || group.commands),
      };
    });
  }

  private normalizeCommands(valueArg: unknown): IHarmonyDeviceCommand[] {
    if (!Array.isArray(valueArg)) {
      return [];
    }
    return valueArg.map((commandArg) => {
      if (typeof commandArg === 'string') {
        return { name: commandArg };
      }
      const command = asRecord(commandArg) || {};
      return {
        name: this.stringValue(command.name ?? command.command) || 'command',
        action: this.stringValue(command.action),
        label: this.stringValue(command.label),
        raw: command,
      };
    });
  }

  private flattenCommands(groupsArg: IHarmonyDeviceCommandGroup[]): IHarmonyDeviceCommand[] {
    return groupsArg.flatMap((groupArg) => groupArg.commands);
  }

  private resolveActivity(snapshotArg: IHarmonySnapshot, activityArg: string): IHarmonyActivity | undefined {
    const normalized = activityArg === harmonySelectPowerOffOption ? harmonyPowerOffActivity : activityArg;
    if (!normalized || normalized === harmonyPreviousActiveActivity) {
      return undefined;
    }
    return snapshotArg.activities.find((activity) => activity.id === normalized || activity.label === normalized);
  }

  private resolveDevice(snapshotArg: IHarmonySnapshot, deviceArg: string): IHarmonyDevice | undefined {
    const normalized = deviceArg.trim();
    return snapshotArg.devices.find((device) => device.id === normalized || device.label === normalized);
  }

  private activityIdForLabel(activitiesArg: IHarmonyActivity[], labelArg: string): string | undefined {
    return activitiesArg.find((activityArg) => activityArg.label === labelArg)?.id;
  }

  private activityLabelForId(activitiesArg: IHarmonyActivity[], idArg: unknown): string | undefined {
    const id = this.stringValue(idArg);
    return id ? activitiesArg.find((activityArg) => activityArg.id === id)?.label : undefined;
  }

  private repeats(valueArg: unknown): number {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) ? Math.max(1, Math.floor(valueArg)) : harmonyDefaultRepeats;
  }

  private positiveNumber(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) && valueArg > 0 ? valueArg : undefined;
  }

  private nonNegativeNumber(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) && valueArg >= 0 ? valueArg : undefined;
  }

  private stringValue(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'string' && valueArg.trim()) {
      return valueArg.trim();
    }
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return String(valueArg);
    }
    return undefined;
  }

  private cloneSnapshot(snapshotArg: IHarmonySnapshot): IHarmonySnapshot {
    return {
      deviceInfo: { ...snapshotArg.deviceInfo },
      state: { ...snapshotArg.state },
      activities: snapshotArg.activities.map((activityArg) => ({ ...activityArg, raw: activityArg.raw ? { ...activityArg.raw } : undefined })),
      devices: snapshotArg.devices.map((deviceArg) => ({
        ...deviceArg,
        commandGroups: deviceArg.commandGroups?.map((groupArg) => ({
          ...groupArg,
          commands: groupArg.commands.map((commandArg) => ({ ...commandArg, raw: commandArg.raw ? { ...commandArg.raw } : undefined })),
        })),
        commands: deviceArg.commands?.map((commandArg) => ({ ...commandArg, raw: commandArg.raw ? { ...commandArg.raw } : undefined })),
        raw: deviceArg.raw ? { ...deviceArg.raw } : undefined,
      })),
      hubConfig: snapshotArg.hubConfig,
      updatedAt: snapshotArg.updatedAt,
    };
  }
}

const asRecord = (valueArg: unknown): Record<string, unknown> | undefined => {
  return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg)
    ? valueArg as Record<string, unknown>
    : undefined;
};

const asArray = (valueArg: unknown): Array<Record<string, unknown>> | undefined => {
  return Array.isArray(valueArg) ? valueArg.filter((itemArg): itemArg is Record<string, unknown> => Boolean(asRecord(itemArg))) : undefined;
};
