import type {
  IChannelsCommand,
  IChannelsCommandResult,
  IChannelsConfig,
  IChannelsDeviceInfo,
  IChannelsFavoriteChannel,
  IChannelsSnapshot,
  IChannelsStatus,
} from './channels.types.js';
import { channelsDefaultPort } from './channels.types.js';

const defaultTimeoutMs = 1000;

export class ChannelsHttpError extends Error {
  constructor(public readonly status: number, messageArg: string) {
    super(messageArg);
    this.name = 'ChannelsHttpError';
  }
}

export class ChannelsClient {
  private currentSnapshot?: IChannelsSnapshot;

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

  public async getSnapshot(): Promise<IChannelsSnapshot> {
    if (this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!this.config.host) {
      if (this.currentSnapshot) {
        return this.cloneSnapshot(this.currentSnapshot);
      }
      this.currentSnapshot = this.normalizeSnapshot(this.snapshotFromConfig(this.hasManualSnapshotData() ? this.config.connected ?? true : false));
      return this.cloneSnapshot(this.currentSnapshot);
    }

    try {
      const status = await this.getStatus();
      const favoriteChannels = await this.getFavoriteChannels().catch(() => [] as IChannelsFavoriteChannel[]);
      this.currentSnapshot = this.normalizeSnapshot({
        deviceInfo: this.deviceInfoFromConfig(),
        status,
        favoriteChannels,
        online: !this.isOfflineStatus(status.status),
        updatedAt: new Date().toISOString(),
      });
    } catch {
      this.currentSnapshot = this.normalizeSnapshot({
        deviceInfo: this.deviceInfoFromConfig(),
        status: { status: 'offline' },
        favoriteChannels: this.config.favoriteChannels || this.currentSnapshot?.favoriteChannels || [],
        online: false,
        updatedAt: new Date().toISOString(),
      });
    }

    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async getStatus(): Promise<IChannelsStatus> {
    if (!this.config.host) {
      return this.clone(this.currentSnapshot?.status || this.config.status || { status: this.config.connected === false ? 'offline' : 'stopped' });
    }
    return this.getJson<IChannelsStatus>('/api/status');
  }

  public async getFavoriteChannels(): Promise<IChannelsFavoriteChannel[]> {
    if (!this.config.host) {
      return this.clone(this.currentSnapshot?.favoriteChannels || this.config.favoriteChannels || []);
    }
    const response = await this.getJson<unknown>('/api/favorite_channels');
    return Array.isArray(response) ? response as IChannelsFavoriteChannel[] : [];
  }

  public async toggleMuted(): Promise<IChannelsStatus> {
    return this.executeApiCommand('toggle_mute', '/api/toggle_mute');
  }

  public async toggleMute(): Promise<IChannelsStatus> {
    return this.toggleMuted();
  }

  public async setMuted(mutedArg: boolean): Promise<IChannelsStatus | undefined> {
    this.ensureCanExecuteCommand();
    const snapshot = await this.getSnapshot();
    if (snapshot.status.muted === mutedArg) {
      return snapshot.status;
    }
    return this.toggleMuted();
  }

  public async toggleCaptions(): Promise<IChannelsStatus> {
    return this.executeApiCommand('toggle_cc', '/api/toggle_cc');
  }

  public async toggleRecord(): Promise<IChannelsStatus> {
    return this.executeApiCommand('toggle_record', '/api/toggle_record');
  }

  public async channelUp(): Promise<IChannelsStatus> {
    return this.executeApiCommand('channel_up', '/api/channel_up');
  }

  public async channelDown(): Promise<IChannelsStatus> {
    return this.executeApiCommand('channel_down', '/api/channel_down');
  }

  public async previousChannel(): Promise<IChannelsStatus> {
    return this.executeApiCommand('previous_channel', '/api/previous_channel');
  }

  public async togglePause(): Promise<IChannelsStatus> {
    return this.executeApiCommand('toggle_pause', '/api/toggle_pause');
  }

  public async pause(): Promise<IChannelsStatus> {
    return this.executeApiCommand('pause', '/api/pause');
  }

  public async resume(): Promise<IChannelsStatus> {
    return this.executeApiCommand('resume', '/api/resume');
  }

  public async stop(): Promise<IChannelsStatus> {
    return this.executeApiCommand('stop', '/api/stop');
  }

  public async seek(secondsArg: number): Promise<IChannelsStatus> {
    const seconds = Math.trunc(secondsArg || 0);
    return this.executeApiCommand('seek', `/api/seek/${encodeURIComponent(String(seconds))}`, { seconds });
  }

  public async seekForward(): Promise<IChannelsStatus> {
    return this.executeApiCommand('seek_forward', '/api/seek_forward');
  }

  public async seekBackward(): Promise<IChannelsStatus> {
    return this.executeApiCommand('seek_backward', '/api/seek_backward');
  }

  public async skipForward(): Promise<IChannelsStatus> {
    return this.executeApiCommand('skip_forward', '/api/skip_forward');
  }

  public async skipBackward(): Promise<IChannelsStatus> {
    return this.executeApiCommand('skip_backward', '/api/skip_backward');
  }

  public async playChannel(channelNumberArg: string | number): Promise<IChannelsStatus> {
    const mediaId = String(channelNumberArg);
    return this.executeApiCommand('play_channel', `/api/play/channel/${encodeURIComponent(mediaId)}`, { mediaId, mediaType: 'channel' });
  }

  public async playRecording(recordingIdArg: string | number): Promise<IChannelsStatus> {
    const mediaId = String(recordingIdArg);
    return this.executeApiCommand('play_recording', `/api/play/recording/${encodeURIComponent(mediaId)}`, { mediaId, mediaType: 'recording' });
  }

  public async navigate(sectionArg: string): Promise<IChannelsStatus> {
    return this.executeApiCommand('navigate', `/api/navigate/${encodeURIComponent(sectionArg)}`, { source: sectionArg });
  }

  public async notify(titleArg: string, messageArg: string): Promise<IChannelsStatus> {
    return this.executeApiCommand('notify', '/api/notify', { body: { title: titleArg, message: messageArg } });
  }

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

  private async executeApiCommand(typeArg: IChannelsCommand['type'], pathArg: string, optionsArg: Partial<IChannelsCommand> = {}): Promise<IChannelsStatus> {
    const command: IChannelsCommand = {
      type: typeArg,
      method: 'POST',
      path: pathArg,
      ...optionsArg,
    };

    if (this.config.commandExecutor) {
      const result = await this.config.commandExecutor(command);
      if (!result.success) {
        throw new Error(result.error || `Channels command ${typeArg} was rejected by commandExecutor.`);
      }
      this.applyCommandResult(result);
      return this.clone(result.status || this.currentSnapshot?.status || { status: 'stopped' });
    }

    this.ensureCanExecuteCommand();
    const response = await this.postJson<IChannelsStatus>(pathArg, command.body);
    if (this.isOfflineStatus(response.status)) {
      throw new Error(`Channels command ${typeArg} returned ${response.status}.`);
    }
    this.patchCachedStatus(response);
    return this.clone(response);
  }

  private applyCommandResult(resultArg: IChannelsCommandResult): void {
    if (resultArg.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneSnapshot(resultArg.snapshot));
      return;
    }
    if (resultArg.status) {
      this.patchCachedStatus(resultArg.status);
    }
  }

  private patchCachedStatus(statusArg: IChannelsStatus): void {
    const base = this.currentSnapshot || this.snapshotFromConfig(true);
    const status = { ...base.status, ...statusArg };
    this.currentSnapshot = this.normalizeSnapshot({
      ...base,
      status,
      online: !this.isOfflineStatus(status.status),
      updatedAt: new Date().toISOString(),
    });
  }

  private async getJson<T>(pathArg: string): Promise<T> {
    return this.requestJson<T>('GET', pathArg);
  }

  private async postJson<T>(pathArg: string, bodyArg?: unknown): Promise<T> {
    return this.requestJson<T>('POST', pathArg, bodyArg);
  }

  private async requestJson<T>(methodArg: 'GET' | 'POST', pathArg: string, bodyArg?: unknown): Promise<T> {
    const response = await this.request(pathArg, {
      method: methodArg,
      headers: { 'content-type': 'application/json', accept: 'application/json' },
      body: bodyArg !== undefined ? JSON.stringify(bodyArg) : undefined,
    });
    const text = await response.text();
    if (!response.ok) {
      throw new ChannelsHttpError(response.status, `Channels request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    if (!text.trim()) {
      return {} as T;
    }
    try {
      return JSON.parse(text) as T;
    } catch {
      return {} as T;
    }
  }

  private async request(pathArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = globalThis.setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
    try {
      return await globalThis.fetch(`${this.baseUrl()}${pathArg}`, {
        ...initArg,
        signal: abortController.signal,
      });
    } finally {
      globalThis.clearTimeout(timeout);
    }
  }

  private ensureCanExecuteCommand(): void {
    if (!this.config.host && !this.config.commandExecutor) {
      throw new Error('Channels media controls require config.host or commandExecutor.');
    }
  }

  private snapshotFromConfig(onlineArg: boolean): IChannelsSnapshot {
    return {
      deviceInfo: this.deviceInfoFromConfig(),
      status: this.config.status || { status: onlineArg ? 'stopped' : 'offline' },
      favoriteChannels: this.config.favoriteChannels || [],
      online: onlineArg,
      updatedAt: new Date().toISOString(),
    };
  }

  private normalizeSnapshot(snapshotArg: IChannelsSnapshot): IChannelsSnapshot {
    const status = { ...(snapshotArg.status || {}) };
    const online = Boolean(snapshotArg.online) && !this.isOfflineStatus(status.status);
    return {
      ...snapshotArg,
      deviceInfo: {
        ...this.deviceInfoFromConfig(),
        ...snapshotArg.deviceInfo,
      },
      status,
      favoriteChannels: Array.isArray(snapshotArg.favoriteChannels) ? snapshotArg.favoriteChannels : [],
      online,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
    };
  }

  private deviceInfoFromConfig(): IChannelsDeviceInfo {
    const endpoint = this.endpointFromHost();
    return {
      ...this.config.deviceInfo,
      id: this.config.deviceInfo?.id || this.config.uniqueId || endpoint.host || this.config.name || 'channels',
      name: this.config.deviceInfo?.name || this.config.name || endpoint.host || 'Channels',
      host: this.config.deviceInfo?.host || endpoint.host,
      port: this.config.deviceInfo?.port || endpoint.port || this.config.port || channelsDefaultPort,
      manufacturer: this.config.deviceInfo?.manufacturer || 'Fancy Bits',
      model: this.config.deviceInfo?.model || 'Channels',
    };
  }

  private baseUrl(): string {
    if (!this.config.host) {
      throw new Error('Channels host is required for HTTP API calls.');
    }
    const endpoint = this.endpointFromHost();
    if (endpoint.url) {
      return endpoint.url;
    }
    return `http://${this.config.host}:${this.config.port || channelsDefaultPort}`;
  }

  private endpointFromHost(): { host?: string; port?: number; url?: string } {
    if (!this.config.host) {
      return { port: this.config.port || channelsDefaultPort };
    }
    const value = /^https?:\/\//i.test(this.config.host) ? this.config.host : `http://${this.config.host}`;
    try {
      const url = new URL(value);
      if (this.config.port && !url.port) {
        url.port = String(this.config.port);
      }
      if (!url.port) {
        url.port = String(channelsDefaultPort);
      }
      url.pathname = '';
      url.search = '';
      url.hash = '';
      return {
        host: url.hostname,
        port: Number(url.port) || channelsDefaultPort,
        url: url.toString().replace(/\/$/, ''),
      };
    } catch {
      return {
        host: this.config.host,
        port: this.config.port || channelsDefaultPort,
      };
    }
  }

  private hasManualSnapshotData(): boolean {
    return Boolean(this.config.status || this.config.favoriteChannels || this.config.deviceInfo);
  }

  private isOfflineStatus(statusArg: unknown): boolean {
    return statusArg === 'offline' || statusArg === 'error';
  }

  private cloneSnapshot(snapshotArg: IChannelsSnapshot): IChannelsSnapshot {
    return this.clone(snapshotArg);
  }

  private clone<T>(valueArg: T): T {
    return JSON.parse(JSON.stringify(valueArg)) as T;
  }
}
