import type {
  IAndroidIpWebcamBinarySensor,
  IAndroidIpWebcamCamera,
  IAndroidIpWebcamClientCommand,
  IAndroidIpWebcamConfig,
  IAndroidIpWebcamDeviceInfo,
  IAndroidIpWebcamSensor,
  IAndroidIpWebcamSensorData,
  IAndroidIpWebcamSnapshot,
  IAndroidIpWebcamSnapshotImage,
  IAndroidIpWebcamStatusData,
  IAndroidIpWebcamSwitch,
  TAndroidIpWebcamOrientation,
  TAndroidIpWebcamProtocol,
  TAndroidIpWebcamRtspAudioCodec,
  TAndroidIpWebcamRtspVideoCodec,
} from './android_ip_webcam.types.js';
import {
  androidIpWebcamDefaultPort,
  androidIpWebcamDefaultTimeoutMs,
  androidIpWebcamSensorDescriptions,
  androidIpWebcamSwitchDescriptions,
} from './android_ip_webcam.types.js';

const allowedOrientations = new Set<TAndroidIpWebcamOrientation>(['landscape', 'upsidedown', 'portrait', 'upsidedown_portrait']);

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

export class AndroidIpWebcamClient {
  private snapshot?: IAndroidIpWebcamSnapshot;

  constructor(private readonly config: IAndroidIpWebcamConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IAndroidIpWebcamSnapshot> {
    if (!forceRefreshArg && this.snapshot) {
      return this.snapshot;
    }

    if (!forceRefreshArg && this.config.snapshot) {
      this.snapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
      return this.snapshot;
    }

    if (this.hasLiveTarget()) {
      try {
        this.snapshot = await this.fetchSnapshot();
        return this.snapshot;
      } catch (errorArg) {
        this.snapshot = this.snapshotFromConfig(false, errorArg instanceof Error ? errorArg.message : String(errorArg));
        return this.snapshot;
      }
    }

    this.snapshot = this.snapshotFromConfig(this.config.connected ?? false);
    return this.snapshot;
  }

  public async validateConnection(): Promise<void> {
    await this.fetchSnapshot();
  }

  public async execute(commandArg: IAndroidIpWebcamClientCommand): Promise<unknown> {
    if (commandArg.type === 'refresh') {
      return this.getSnapshot(true);
    }
    if (commandArg.type === 'stream_source') {
      return {
        streamSource: this.rtspUrl(commandArg.videoCodec || 'h264', commandArg.audioCodec || 'aac'),
        mjpegUrl: this.mjpegUrl(),
        stillImageUrl: this.imageUrl(),
        verified: false,
      };
    }
    if (commandArg.type === 'snapshot_image') {
      if (commandArg.filename) {
        throw new Error('Android IP Webcam snapshot file writes are not implemented; request data as base64 without data.filename.');
      }
      const image = await this.getSnapshotImage();
      return {
        contentType: image.contentType,
        dataBase64: Buffer.from(image.data).toString('base64'),
      };
    }
    if (commandArg.type === 'setting') {
      if (!commandArg.key) {
        throw new Error('Android IP Webcam setting command requires a key.');
      }
      await this.changeSetting(commandArg.key, commandArg.value);
      return { ok: true, key: commandArg.key, value: commandArg.value };
    }
    if (commandArg.type === 'torch') {
      await this.getOk(commandArg.activate === false ? '/disabletorch' : '/enabletorch', 'torch');
      this.patchCachedSetting('torch', commandArg.activate !== false);
      return { ok: true, key: 'torch', value: commandArg.activate !== false };
    }
    if (commandArg.type === 'focus') {
      await this.getOk(commandArg.activate === false ? '/nofocus' : '/focus', 'focus');
      this.patchCachedSetting('focus', commandArg.activate !== false);
      return { ok: true, key: 'focus', value: commandArg.activate !== false };
    }
    if (commandArg.type === 'record') {
      await this.record(commandArg.record !== false, commandArg.tag);
      return { ok: true, key: 'video_recording', value: commandArg.record !== false };
    }
    if (commandArg.type === 'set_zoom') {
      if (typeof commandArg.zoom !== 'number' || !Number.isFinite(commandArg.zoom)) {
        throw new Error('Android IP Webcam set_zoom requires a numeric zoom value.');
      }
      await this.getOk(`/settings/ptz?zoom=${encodeURIComponent(String(Math.round(commandArg.zoom)))}`, 'set_zoom');
      return { ok: true, key: 'zoom', value: Math.round(commandArg.zoom) };
    }
    if (commandArg.type === 'set_quality') {
      if (typeof commandArg.quality !== 'number' || !Number.isFinite(commandArg.quality)) {
        throw new Error('Android IP Webcam set_quality requires a numeric quality value.');
      }
      const quality = Math.max(0, Math.min(100, Math.round(commandArg.quality)));
      await this.changeSetting('quality', quality);
      return { ok: true, key: 'quality', value: quality };
    }
    if (commandArg.type === 'set_orientation') {
      if (!commandArg.orientation || !allowedOrientations.has(commandArg.orientation)) {
        throw new Error('Android IP Webcam set_orientation requires a supported orientation.');
      }
      await this.changeSetting('orientation', commandArg.orientation);
      return { ok: true, key: 'orientation', value: commandArg.orientation };
    }
    if (commandArg.type === 'set_scenemode') {
      if (!commandArg.scenemode) {
        throw new Error('Android IP Webcam set_scenemode requires a scenemode value.');
      }
      await this.changeSetting('scenemode', commandArg.scenemode);
      return { ok: true, key: 'scenemode', value: commandArg.scenemode };
    }
    throw new Error(`Unsupported Android IP Webcam command: ${commandArg.type}`);
  }

  public async getSnapshotImage(): Promise<IAndroidIpWebcamSnapshotImage> {
    const response = await this.request('/shot.jpg');
    return {
      contentType: response.headers.get('content-type') || 'image/jpeg',
      data: new Uint8Array(await response.arrayBuffer()),
    };
  }

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

  private async fetchSnapshot(): Promise<IAndroidIpWebcamSnapshot> {
    const [statusData, sensorData] = await Promise.all([
      this.requestJson<IAndroidIpWebcamStatusData>('/status.json?show_avail=1'),
      this.requestJson<IAndroidIpWebcamSensorData>('/sensors.json'),
    ]);
    return this.snapshotFromData(statusData, sensorData, true);
  }

  private snapshotFromData(statusDataArg: IAndroidIpWebcamStatusData, sensorDataArg: IAndroidIpWebcamSensorData, connectedArg: boolean): IAndroidIpWebcamSnapshot {
    const currentSettings = this.currentSettings(statusDataArg.curvals || this.config.currentSettings || {});
    const enabledSensors = this.config.enabledSensors || Object.keys(sensorDataArg);
    const enabledSettings = this.config.enabledSettings || Object.keys(currentSettings);
    const availableSettings = this.availableSettings(statusDataArg.avail || this.config.availableSettings || {});
    const deviceInfo = this.deviceInfo(connectedArg);
    const snapshot: IAndroidIpWebcamSnapshot = {
      deviceInfo,
      camera: this.camera(deviceInfo, connectedArg),
      sensors: this.config.sensors || this.sensors(statusDataArg, sensorDataArg, enabledSensors, connectedArg),
      binarySensors: this.config.binarySensors || this.binarySensors(sensorDataArg, enabledSensors, connectedArg),
      switches: this.config.switches || this.switches(currentSettings, enabledSettings, connectedArg),
      statusData: statusDataArg,
      sensorData: sensorDataArg,
      currentSettings,
      enabledSensors,
      enabledSettings,
      availableSettings,
      connected: connectedArg,
      updatedAt: new Date().toISOString(),
    };
    return this.normalizeSnapshot(snapshot);
  }

  private snapshotFromConfig(connectedArg: boolean, lastErrorArg?: string): IAndroidIpWebcamSnapshot {
    const statusData = this.config.statusData || this.config.snapshot?.statusData || {};
    const sensorData = this.config.sensorData || this.config.snapshot?.sensorData || {};
    const currentSettings = this.currentSettings(this.config.currentSettings || this.config.snapshot?.currentSettings || statusData.curvals || {});
    const enabledSensors = this.config.enabledSensors || this.config.snapshot?.enabledSensors || Object.keys(sensorData);
    const enabledSettings = this.config.enabledSettings || this.config.snapshot?.enabledSettings || Object.keys(currentSettings);
    const availableSettings = this.availableSettings(this.config.availableSettings || this.config.snapshot?.availableSettings || statusData.avail || {});
    const deviceInfo = this.deviceInfo(connectedArg);
    return this.normalizeSnapshot({
      deviceInfo,
      camera: this.config.camera || this.config.snapshot?.camera || this.camera(deviceInfo, connectedArg),
      sensors: this.config.sensors || this.config.snapshot?.sensors || this.sensors(statusData, sensorData, enabledSensors, connectedArg),
      binarySensors: this.config.binarySensors || this.config.snapshot?.binarySensors || this.binarySensors(sensorData, enabledSensors, connectedArg),
      switches: this.config.switches || this.config.snapshot?.switches || this.switches(currentSettings, enabledSettings, connectedArg),
      statusData,
      sensorData,
      currentSettings,
      enabledSensors,
      enabledSettings,
      availableSettings,
      connected: connectedArg,
      updatedAt: new Date().toISOString(),
      metadata: {
        ...this.config.snapshot?.metadata,
        lastLiveError: lastErrorArg,
      },
    });
  }

  private normalizeSnapshot(snapshotArg: IAndroidIpWebcamSnapshot): IAndroidIpWebcamSnapshot {
    const connected = Boolean(snapshotArg.connected && snapshotArg.deviceInfo.online !== false);
    const deviceInfo = {
      ...this.deviceInfo(connected),
      ...snapshotArg.deviceInfo,
      online: connected,
    };
    return {
      ...snapshotArg,
      deviceInfo,
      camera: {
        ...this.camera(deviceInfo, connected),
        ...snapshotArg.camera,
        available: connected && snapshotArg.camera.available !== false,
      },
      sensors: snapshotArg.sensors || [],
      binarySensors: snapshotArg.binarySensors || [],
      switches: snapshotArg.switches || [],
      statusData: snapshotArg.statusData || {},
      sensorData: snapshotArg.sensorData || {},
      currentSettings: snapshotArg.currentSettings || {},
      enabledSensors: snapshotArg.enabledSensors || [],
      enabledSettings: snapshotArg.enabledSettings || [],
      availableSettings: snapshotArg.availableSettings || {},
      connected,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
    };
  }

  private sensors(statusDataArg: IAndroidIpWebcamStatusData, sensorDataArg: IAndroidIpWebcamSensorData, enabledSensorsArg: string[], connectedArg: boolean): IAndroidIpWebcamSensor[] {
    const enabled = new Set(enabledSensorsArg);
    const sensors: IAndroidIpWebcamSensor[] = [];
    for (const description of androidIpWebcamSensorDescriptions) {
      const isConnectionSensor = Boolean(description.statusKey);
      if (!isConnectionSensor && !enabled.has(description.key)) {
        continue;
      }
      const sensorDatum = sensorDataArg[description.key];
      const value = description.statusKey ? statusDataArg[description.statusKey] : this.sensorValue(sensorDatum);
      sensors.push({
        key: description.key,
        name: description.name,
        value,
        unit: sensorDatum?.unit,
        deviceClass: description.deviceClass,
        stateClass: description.stateClass,
        entityCategory: description.entityCategory,
        available: connectedArg && (isConnectionSensor || enabled.has(description.key)),
      });
    }
    return sensors;
  }

  private binarySensors(sensorDataArg: IAndroidIpWebcamSensorData, enabledSensorsArg: string[], connectedArg: boolean): IAndroidIpWebcamBinarySensor[] {
    const enabled = enabledSensorsArg.includes('motion_active');
    return [{
      key: 'motion_active',
      name: 'Motion active',
      isOn: this.sensorValue(sensorDataArg.motion_active) === 1 || this.sensorValue(sensorDataArg.motion_active) === 1.0,
      deviceClass: 'motion',
      available: connectedArg && enabled,
    }];
  }

  private switches(currentSettingsArg: Record<string, unknown>, enabledSettingsArg: string[], connectedArg: boolean): IAndroidIpWebcamSwitch[] {
    const enabled = new Set(enabledSettingsArg);
    return androidIpWebcamSwitchDescriptions
      .filter((descriptionArg) => enabled.has(descriptionArg.key))
      .map((descriptionArg): IAndroidIpWebcamSwitch => ({
        key: descriptionArg.key,
        name: descriptionArg.name,
        isOn: Boolean(currentSettingsArg[descriptionArg.key]),
        command: descriptionArg.command,
        entityCategory: descriptionArg.entityCategory,
        available: connectedArg,
      }));
  }

  private sensorValue(sensorDatumArg: unknown): unknown {
    const data = record(sensorDatumArg)?.data;
    if (!Array.isArray(data) || !data.length) {
      return undefined;
    }
    const series = data[data.length - 1];
    if (!Array.isArray(series) || !series.length) {
      return series;
    }
    const sample = series[series.length - 1];
    if (Array.isArray(sample)) {
      return sample[0];
    }
    return sample;
  }

  private currentSettings(valuesArg: Record<string, unknown>): Record<string, unknown> {
    return Object.fromEntries(Object.entries(valuesArg).map(([keyArg, valueArg]) => [keyArg, this.settingValue(valueArg)]));
  }

  private availableSettings(valuesArg: Record<string, unknown[]>): Record<string, unknown[]> {
    return Object.fromEntries(Object.entries(valuesArg).map(([keyArg, valueArg]) => [keyArg, Array.isArray(valueArg) ? valueArg.map((itemArg) => this.settingValue(itemArg)) : []]));
  }

  private settingValue(valueArg: unknown): unknown {
    if (typeof valueArg !== 'string') {
      return valueArg;
    }
    const value = valueArg.trim();
    if (value === 'on') {
      return true;
    }
    if (value === 'off') {
      return false;
    }
    if (/^-?\d+(?:\.\d+)?$/.test(value)) {
      return Number(value);
    }
    return value;
  }

  private async changeSetting(keyArg: string, valueArg: unknown): Promise<void> {
    const payload = typeof valueArg === 'boolean' ? valueArg ? 'on' : 'off' : String(valueArg ?? '');
    await this.getOk(`/settings/${encodeURIComponent(keyArg)}?set=${encodeURIComponent(payload)}`, `change_setting ${keyArg}`);
    this.patchCachedSetting(keyArg, valueArg);
  }

  private async record(recordArg: boolean, tagArg?: string): Promise<void> {
    const tag = tagArg ? `&tag=${encodeURIComponent(tagArg)}` : '';
    await this.getOk(recordArg ? `/startvideo?force=1${tag}` : '/stopvideo?force=1', 'record');
    this.patchCachedSetting('video_recording', recordArg);
  }

  private async getOk(pathArg: string, actionArg: string): Promise<void> {
    const text = await (await this.request(pathArg)).text();
    if (!text.includes('Ok')) {
      throw new Error(`Android IP Webcam ${actionArg} did not return Ok.`);
    }
  }

  private async requestJson<TValue>(pathArg: string): Promise<TValue> {
    const value = await (await this.request(pathArg)).json();
    return record(value) ? value as TValue : {} as TValue;
  }

  private async request(pathArg: string): Promise<Response> {
    const baseUrl = this.baseUrl();
    if (!baseUrl) {
      throw new Error('Android IP Webcam live HTTP client requires config.host or config.url.');
    }
    const headers = new Headers();
    const authorization = this.basicAuthorization();
    if (authorization) {
      headers.set('authorization', authorization);
    }
    const response = await this.fetchWithTimeout(`${baseUrl}${pathArg.startsWith('/') ? pathArg : `/${pathArg}`}`, { method: 'GET', headers });
    if (!response.ok) {
      const text = await response.text().catch(() => '');
      if (response.status === 401) {
        throw new AndroidIpWebcamHttpError(response.status, 'Android IP Webcam authentication failed.');
      }
      throw new AndroidIpWebcamHttpError(response.status, `Android IP Webcam request ${pathArg} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }
    return response;
  }

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

  private patchCachedSetting(keyArg: string, valueArg: unknown): void {
    if (!this.snapshot) {
      return;
    }
    this.snapshot.currentSettings[keyArg] = valueArg;
    for (const switchEntity of this.snapshot.switches) {
      if (switchEntity.key === keyArg) {
        switchEntity.isOn = Boolean(valueArg);
      }
    }
  }

  private camera(deviceInfoArg: IAndroidIpWebcamDeviceInfo, connectedArg: boolean): IAndroidIpWebcamCamera {
    return {
      id: 'camera',
      name: `${deviceInfoArg.name || 'Android IP Webcam'} Camera`,
      mjpegUrl: this.mjpegUrl(),
      imageUrl: this.imageUrl(),
      rtspUrl: this.safeRtspUrl('h264', 'aac'),
      audioWavUrl: this.audioUrl('audio.wav'),
      audioAacUrl: this.audioUrl('audio.aac'),
      audioOpusUrl: this.audioUrl('audio.opus'),
      supportedFeatures: ['stream'],
      available: connectedArg,
    };
  }

  private deviceInfo(connectedArg: boolean): IAndroidIpWebcamDeviceInfo {
    const endpoint = this.endpoint();
    return {
      ...this.config.deviceInfo,
      id: this.config.deviceInfo?.id || this.config.uniqueId || endpoint.host || this.config.url || 'manual-android-ip-webcam',
      name: this.config.deviceInfo?.name || this.config.name || endpoint.host || this.config.url || 'Android IP Webcam',
      manufacturer: this.config.deviceInfo?.manufacturer || this.config.manufacturer || 'Android IP Webcam',
      model: this.config.deviceInfo?.model || this.config.model,
      host: this.config.deviceInfo?.host || endpoint.host,
      port: this.config.deviceInfo?.port || endpoint.port,
      protocol: this.config.deviceInfo?.protocol || endpoint.protocol,
      url: this.config.deviceInfo?.url || this.baseUrl(),
      online: connectedArg,
    };
  }

  private mjpegUrl(): string | undefined {
    const baseUrl = this.baseUrl();
    return baseUrl ? `${baseUrl}/video` : undefined;
  }

  private imageUrl(): string | undefined {
    const baseUrl = this.baseUrl();
    return baseUrl ? `${baseUrl}/shot.jpg` : undefined;
  }

  private audioUrl(pathArg: 'audio.wav' | 'audio.aac' | 'audio.opus'): string | undefined {
    const baseUrl = this.baseUrl();
    return baseUrl ? `${baseUrl}/${pathArg}` : undefined;
  }

  private rtspUrl(videoCodecArg: TAndroidIpWebcamRtspVideoCodec, audioCodecArg: TAndroidIpWebcamRtspAudioCodec): string | undefined {
    const endpoint = this.endpoint();
    if (!endpoint.host) {
      throw new Error('Android IP Webcam stream_source requires config.host or config.url.');
    }
    const protocol = endpoint.protocol === 'https' ? 'rtsps' : 'rtsp';
    const credentials = this.rtspCredentials();
    return `${protocol}://${credentials}${endpoint.host}:${endpoint.port || androidIpWebcamDefaultPort}/${videoCodecArg}_${audioCodecArg}.sdp`;
  }

  private safeRtspUrl(videoCodecArg: TAndroidIpWebcamRtspVideoCodec, audioCodecArg: TAndroidIpWebcamRtspAudioCodec): string | undefined {
    try {
      return this.rtspUrl(videoCodecArg, audioCodecArg);
    } catch {
      return undefined;
    }
  }

  private baseUrl(): string | undefined {
    if (this.config.url) {
      const url = safeUrl(this.config.url);
      if (url) {
        return url.toString().replace(/\/$/, '');
      }
    }
    const endpoint = this.endpoint();
    if (!endpoint.host) {
      return undefined;
    }
    return `${endpoint.protocol}://${endpoint.host}:${endpoint.port || androidIpWebcamDefaultPort}`;
  }

  private endpoint(): { protocol: TAndroidIpWebcamProtocol; host?: string; port?: number } {
    const url = safeUrl(this.config.url || this.config.host);
    if (url) {
      return {
        protocol: url.protocol === 'https:' ? 'https' : 'http',
        host: url.hostname,
        port: url.port ? Number(url.port) : url.protocol === 'https:' ? 443 : androidIpWebcamDefaultPort,
      };
    }
    return {
      protocol: this.config.protocol || 'http',
      host: this.config.host,
      port: this.config.port || androidIpWebcamDefaultPort,
    };
  }

  private basicAuthorization(): string | undefined {
    if (!this.config.username || this.config.password === undefined) {
      return undefined;
    }
    return `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`, 'utf8').toString('base64')}`;
  }

  private rtspCredentials(): string {
    if (!this.config.username || this.config.password === undefined) {
      return '';
    }
    return `${encodeURIComponent(this.config.username)}:${encodeURIComponent(this.config.password)}@`;
  }

  private hasLiveTarget(): boolean {
    return Boolean(this.baseUrl());
  }

  private cloneSnapshot(snapshotArg: IAndroidIpWebcamSnapshot): IAndroidIpWebcamSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IAndroidIpWebcamSnapshot;
  }
}

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

const safeUrl = (valueArg: string | undefined): URL | undefined => {
  if (!valueArg) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
