import {
  androidtvRemoteApiPort,
  androidtvRemoteKeyAliases,
  androidtvRemoteKnownApps,
  androidtvRemotePairPort,
} from './androidtv_remote.constants.js';
import type {
  IAndroidtvRemoteApp,
  IAndroidtvRemoteCommand,
  IAndroidtvRemoteCommandContext,
  IAndroidtvRemoteConfig,
  IAndroidtvRemoteConfiguredApp,
  IAndroidtvRemoteDeviceInfo,
  IAndroidtvRemoteDeviceState,
  IAndroidtvRemoteKeyPress,
  IAndroidtvRemoteSnapshot,
  IAndroidtvRemoteVolumeInfo,
  TAndroidtvRemoteCommandDirection,
  TAndroidtvRemoteCommandExecutor,
  TAndroidtvRemoteCommandReason,
  TAndroidtvRemoteKeyCode,
} from './androidtv_remote.types.js';

export class AndroidtvRemoteUnsupportedProtocolError extends Error {
  constructor(commandArg: IAndroidtvRemoteCommand) {
    super(`Android TV Remote protocol action "${commandArg.action}" requires an injected executor. This TypeScript port does not implement pairing or live androidtvremote2 transport.`);
    this.name = 'AndroidtvRemoteUnsupportedProtocolError';
  }
}

export class AndroidtvRemoteClient {
  private readonly snapshot?: IAndroidtvRemoteSnapshot;

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

  public async getSnapshot(): Promise<IAndroidtvRemoteSnapshot> {
    return this.normalizeSnapshot(this.snapshot || this.snapshotFromManualConfig());
  }

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

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

  public async finishPairing(pinArg: string): Promise<void> {
    await this.execute({ action: 'finish_pairing', reason: 'finish_pairing', pin: pinArg });
  }

  public async turnOn(): Promise<void> {
    await this.sendKeyCommand('POWER', 'SHORT', 'turn_on');
  }

  public async turnOff(): Promise<void> {
    await this.sendKeyCommand('POWER', 'SHORT', 'turn_off');
  }

  public async volumeUp(): Promise<void> {
    await this.sendKeyCommand('VOLUME_UP', 'SHORT', 'volume_up');
  }

  public async volumeDown(): Promise<void> {
    await this.sendKeyCommand('VOLUME_DOWN', 'SHORT', 'volume_down');
  }

  public async muteVolume(mutedArg: boolean): Promise<void> {
    await this.sendKeyCommand('VOLUME_MUTE', 'SHORT', 'volume_mute', { muted: mutedArg });
  }

  public async setVolumeLevel(volumeLevelArg: number): Promise<void> {
    const volumeLevel = Math.max(0, Math.min(1, volumeLevelArg));
    await this.execute({ action: 'volume_set', reason: 'volume_set', volumeLevel });
  }

  public async mediaPlay(): Promise<void> {
    await this.sendKeyCommand('MEDIA_PLAY', 'SHORT', 'media_play');
  }

  public async mediaPause(): Promise<void> {
    await this.sendKeyCommand('MEDIA_PAUSE', 'SHORT', 'media_pause');
  }

  public async mediaPlayPause(): Promise<void> {
    await this.sendKeyCommand('MEDIA_PLAY_PAUSE', 'SHORT', 'media_play_pause');
  }

  public async mediaStop(): Promise<void> {
    await this.sendKeyCommand('MEDIA_STOP', 'SHORT', 'media_stop');
  }

  public async mediaPreviousTrack(): Promise<void> {
    await this.sendKeyCommand('MEDIA_PREVIOUS', 'SHORT', 'media_previous_track');
  }

  public async mediaNextTrack(): Promise<void> {
    await this.sendKeyCommand('MEDIA_NEXT', 'SHORT', 'media_next_track');
  }

  public async playChannel(channelArg: string): Promise<void> {
    if (!/^\d+$/.test(channelArg)) {
      throw new Error(`Android TV Remote channel media_id must be numeric: ${channelArg}`);
    }
    await this.sendCommand(channelArg.split(''), { reason: 'play_channel' });
  }

  public async launchApp(appLinkOrAppIdArg: string, reasonArg: TAndroidtvRemoteCommandReason = 'launch_app'): Promise<void> {
    const app = await this.appForActivity(appLinkOrAppIdArg);
    const appId = app?.id || (this.hasUrlScheme(appLinkOrAppIdArg) ? undefined : appLinkOrAppIdArg);
    const appLink = app?.link || (this.hasUrlScheme(appLinkOrAppIdArg) ? appLinkOrAppIdArg : `market://launch?id=${appLinkOrAppIdArg}`);
    await this.execute({
      action: 'launch_app',
      reason: reasonArg,
      appId,
      appLink,
      appName: app?.name || (appId ? androidtvRemoteKnownApps[appId] : undefined),
    });
  }

  public async selectActivity(activityArg: string): Promise<void> {
    const app = await this.appForActivity(activityArg);
    await this.launchApp(app?.id || activityArg, 'select_activity');
  }

  public async sendText(textArg: string): Promise<void> {
    await this.execute({ action: 'send_text', reason: 'send_text', text: textArg });
  }

  public async sendKeyCommand(
    keyCodeArg: TAndroidtvRemoteKeyCode | string,
    directionArg: TAndroidtvRemoteCommandDirection = 'SHORT',
    reasonArg: TAndroidtvRemoteCommandReason = 'remote_send_command',
    extraArg: Partial<IAndroidtvRemoteCommand> = {}
  ): Promise<void> {
    await this.execute({
      action: 'key_command',
      reason: reasonArg,
      keyCode: this.normalizeKeyCode(keyCodeArg),
      direction: directionArg,
      ...extraArg,
    });
  }

  public async sendCommand(
    commandsArg: Array<TAndroidtvRemoteKeyCode | string>,
    optionsArg: {
      repeats?: number;
      delaySecs?: number;
      holdSecs?: number;
      reason?: TAndroidtvRemoteCommandReason;
    } = {}
  ): Promise<void> {
    const keys: IAndroidtvRemoteKeyPress[] = commandsArg.flatMap((keyArg): IAndroidtvRemoteKeyPress[] => {
      const keyCode = this.normalizeKeyCode(keyArg);
      if (optionsArg.holdSecs) {
        return [
          { keyCode, direction: 'START_LONG' },
          { keyCode, direction: 'END_LONG' },
        ];
      }
      return [{ keyCode, direction: 'SHORT' }];
    });
    await this.execute({
      action: 'remote_send_command',
      reason: optionsArg.reason || 'remote_send_command',
      keys,
      repeats: this.repeats(optionsArg.repeats),
      delaySecs: optionsArg.delaySecs,
      holdSecs: optionsArg.holdSecs,
    });
  }

  public async destroy(): Promise<void> {}

  private async execute(commandArg: IAndroidtvRemoteCommand): Promise<void> {
    const executor = this.config.executor;
    if (!executor) {
      throw new AndroidtvRemoteUnsupportedProtocolError(commandArg);
    }
    const context: IAndroidtvRemoteCommandContext = {
      config: this.config,
      snapshot: await this.getSnapshot(),
    };
    if (typeof executor === 'function') {
      await executor(commandArg, context);
      return;
    }
    await executor.execute(commandArg, context);
  }

  private snapshotFromManualConfig(): IAndroidtvRemoteSnapshot {
    const deviceInfo: IAndroidtvRemoteDeviceInfo = {
      ...this.config.deviceInfo,
      host: this.config.deviceInfo?.host || this.config.host,
      apiPort: this.config.deviceInfo?.apiPort || this.config.apiPort || androidtvRemoteApiPort,
      pairPort: this.config.deviceInfo?.pairPort || this.config.pairPort || androidtvRemotePairPort,
      name: this.config.deviceInfo?.name || this.config.deviceName || this.config.host || 'Android TV Remote',
      macAddress: this.config.deviceInfo?.macAddress || this.config.macAddress,
      manufacturer: this.config.deviceInfo?.manufacturer || this.config.manufacturer,
      model: this.config.deviceInfo?.model || this.config.model,
    };
    const state: IAndroidtvRemoteDeviceState = {
      mediaState: 'unknown',
      available: false,
      ...this.config.state,
      volumeInfo: this.config.state?.volumeInfo || this.config.volumeInfo,
    };
    return {
      deviceInfo,
      state,
      apps: this.normalizeApps(this.config.apps),
    };
  }

  private normalizeSnapshot(snapshotArg: IAndroidtvRemoteSnapshot): IAndroidtvRemoteSnapshot {
    const deviceInfo: IAndroidtvRemoteDeviceInfo = {
      ...snapshotArg.deviceInfo,
      host: snapshotArg.deviceInfo.host || this.config.host,
      apiPort: snapshotArg.deviceInfo.apiPort || this.config.apiPort || androidtvRemoteApiPort,
      pairPort: snapshotArg.deviceInfo.pairPort || this.config.pairPort || androidtvRemotePairPort,
      macAddress: snapshotArg.deviceInfo.macAddress || this.config.macAddress,
    };
    if (!deviceInfo.name) {
      deviceInfo.name = this.config.deviceName || deviceInfo.host || 'Android TV Remote';
    }
    const apps = this.normalizeApps(snapshotArg.apps.length ? snapshotArg.apps : this.config.apps);
    const volumeInfo = this.normalizeVolumeInfo(snapshotArg.state.volumeInfo || this.config.volumeInfo);
    const state: IAndroidtvRemoteDeviceState = {
      ...snapshotArg.state,
      volumeInfo,
    };
    if (state.available === undefined) {
      state.available = state.isOn !== undefined || Boolean(state.currentApp || volumeInfo);
    }
    if (!state.currentAppName && state.currentApp) {
      state.currentAppName = this.appName(apps, state.currentApp);
    }
    if (!state.currentActivity) {
      state.currentActivity = state.currentAppName || state.currentApp;
    }
    if (state.isVolumeMuted === undefined && volumeInfo?.muted !== undefined) {
      state.isVolumeMuted = volumeInfo.muted;
    }
    if (state.volumeLevel === undefined) {
      state.volumeLevel = this.volumeLevel(volumeInfo);
    }
    return {
      deviceInfo,
      state,
      apps,
      updatedAt: snapshotArg.updatedAt,
    };
  }

  private normalizeApps(appsArg: IAndroidtvRemoteConfig['apps']): IAndroidtvRemoteApp[] {
    if (!appsArg) {
      return [];
    }
    if (Array.isArray(appsArg)) {
      return appsArg.map((appArg) => ({
        ...appArg,
        name: appArg.name || androidtvRemoteKnownApps[appArg.id],
      }));
    }
    return Object.entries(appsArg).map(([id, appArg]) => this.normalizeConfiguredApp(id, appArg));
  }

  private normalizeConfiguredApp(idArg: string, appArg: IAndroidtvRemoteConfiguredApp): IAndroidtvRemoteApp {
    return {
      id: idArg,
      name: appArg.name || appArg.appName || appArg.app_name || androidtvRemoteKnownApps[idArg],
      icon: appArg.icon || appArg.appIcon || appArg.app_icon,
      link: appArg.link,
    };
  }

  private normalizeVolumeInfo(volumeInfoArg?: IAndroidtvRemoteVolumeInfo): IAndroidtvRemoteVolumeInfo | undefined {
    return volumeInfoArg ? { ...volumeInfoArg } : undefined;
  }

  private volumeLevel(volumeInfoArg?: IAndroidtvRemoteVolumeInfo): number | undefined {
    if (!volumeInfoArg) {
      return undefined;
    }
    if (typeof volumeInfoArg.max === 'number' && volumeInfoArg.max > 0 && typeof volumeInfoArg.level === 'number') {
      return volumeInfoArg.level / volumeInfoArg.max;
    }
    return undefined;
  }

  private async appForActivity(activityArg: string): Promise<IAndroidtvRemoteApp | undefined> {
    const snapshot = await this.getSnapshot();
    return snapshot.apps.find((appArg) => activityArg === appArg.id || activityArg === appArg.name || activityArg === appArg.link);
  }

  private appName(appsArg: IAndroidtvRemoteApp[], appIdArg: string): string | undefined {
    return appsArg.find((appArg) => appArg.id === appIdArg)?.name || androidtvRemoteKnownApps[appIdArg];
  }

  private normalizeKeyCode(keyCodeArg: TAndroidtvRemoteKeyCode | string): TAndroidtvRemoteKeyCode | string {
    const raw = String(keyCodeArg).trim();
    if (!raw) {
      return raw;
    }
    const withoutPrefix = raw.toUpperCase().replace(/^KEYCODE_/, '').replace(/[\s-]+/g, '_');
    return androidtvRemoteKeyAliases[withoutPrefix] || withoutPrefix;
  }

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

  private hasUrlScheme(valueArg: string): boolean {
    return /^[a-z][a-z0-9+.-]*:/i.test(valueArg);
  }

  private cloneSnapshot(snapshotArg: IAndroidtvRemoteSnapshot): IAndroidtvRemoteSnapshot {
    return {
      deviceInfo: { ...snapshotArg.deviceInfo },
      state: {
        ...snapshotArg.state,
        volumeInfo: snapshotArg.state.volumeInfo ? { ...snapshotArg.state.volumeInfo } : undefined,
      },
      apps: snapshotArg.apps.map((appArg) => ({ ...appArg })),
      updatedAt: snapshotArg.updatedAt,
    };
  }
}
