import { DoorbirdMapper } from './doorbird.mapper.js';
import type {
  IDoorbirdClientLike,
  IDoorbirdCommandRequest,
  IDoorbirdConfig,
  IDoorbirdFavoriteChange,
  IDoorbirdFavorites,
  IDoorbirdInfo,
  IDoorbirdRawData,
  IDoorbirdRefreshResult,
  IDoorbirdScheduleEntry,
  IDoorbirdSnapshot,
  IDoorbirdSnapshotImage,
  IDoorbirdStatus,
  TDoorbirdProtocol,
  TDoorbirdCameraKind,
} from './doorbird.types.js';
import { doorbirdDefaultPort, doorbirdDefaultSnapshotTimeoutMs, doorbirdDefaultTimeoutMs } from './doorbird.types.js';

export class DoorbirdApiError extends Error {}
export class DoorbirdApiConnectionError extends DoorbirdApiError {}
export class DoorbirdApiAuthorizationError extends DoorbirdApiError {}
export class DoorbirdApiNotFoundError extends DoorbirdApiError {}

export class DoorbirdClient {
  private currentSnapshot?: IDoorbirdSnapshot;

  constructor(private readonly config: IDoorbirdConfig) {}

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

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = DoorbirdMapper.toSnapshot({ config: this.config, source: 'snapshot', online: this.config.snapshot.online });
      return this.cloneSnapshot(this.currentSnapshot!);
    }

    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient(this.config.client);
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot!);
    }

    if (this.hasManualData()) {
      this.currentSnapshot = DoorbirdMapper.toSnapshot({ config: this.config, online: this.config.online ?? true, source: 'manual' });
      return this.cloneSnapshot(this.currentSnapshot!);
    }

    if (this.config.host) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot!);
    }

    this.currentSnapshot = this.offlineSnapshot('No DoorBird HTTP endpoint, client, or snapshot/manual data is configured.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IDoorbirdRefreshResult> {
    if (this.config.snapshot || this.config.client || this.hasManualData()) {
      const snapshot = await this.getSnapshot();
      return { success: snapshot.online && !snapshot.error, snapshot, data: { source: snapshot.source || 'runtime' } };
    }

    if (!this.config.host) {
      const snapshot = await this.getSnapshot(true);
      return { success: false, snapshot, error: 'DoorBird refresh requires a configured HTTP endpoint, client, or snapshot/manual data.' };
    }

    try {
      const snapshot = await this.fetchSnapshot();
      this.currentSnapshot = snapshot;
      return { success: true, snapshot: this.cloneSnapshot(snapshot), data: { source: 'http' } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: this.cloneSnapshot(snapshot), error };
    }
  }

  public async validateConnection(): Promise<IDoorbirdInfo> {
    return this.getInfo();
  }

  public async fetchSnapshot(): Promise<IDoorbirdSnapshot> {
    const info = await this.getInfo();
    const [doorbell, motion, favorites, schedule] = await Promise.all([
      this.getDoorbellState().catch(() => undefined),
      this.getMotionSensorState().catch(() => undefined),
      this.getFavorites().catch(() => undefined),
      this.getSchedule().catch((errorArg) => errorArg instanceof DoorbirdApiNotFoundError ? undefined : undefined),
    ]);
    return DoorbirdMapper.toSnapshot({
      config: this.config,
      rawData: {
        info,
        status: {
          doorbell,
          motion,
          updatedAt: new Date().toISOString(),
        },
        favorites,
        schedule,
      },
      online: true,
      source: 'http',
    });
  }

  public async getInfo(): Promise<IDoorbirdInfo> {
    const data = await this.requestJson<Record<string, unknown>>('/bha-api/info.cgi');
    const bha = recordValue(data.BHA);
    const version = Array.isArray(bha?.VERSION) ? bha.VERSION[0] : undefined;
    if (!version || typeof version !== 'object') {
      throw new DoorbirdApiConnectionError('DoorBird info response did not contain BHA.VERSION[0].');
    }
    return version as IDoorbirdInfo;
  }

  public async getDoorbellState(): Promise<boolean> {
    return this.monitorState('doorbell');
  }

  public async getMotionSensorState(): Promise<boolean> {
    return this.monitorState('motionsensor');
  }

  public async getFavorites(): Promise<IDoorbirdFavorites> {
    return this.requestJson<IDoorbirdFavorites>('/bha-api/favorites.cgi');
  }

  public async changeFavorite(favoriteArg: IDoorbirdFavoriteChange): Promise<boolean> {
    const query: Record<string, string | number | undefined> = {
      action: 'save',
      type: favoriteArg.type,
      title: favoriteArg.title,
      value: favoriteArg.value,
      id: favoriteArg.id,
    };
    await this.request('/bha-api/favorites.cgi', { query });
    return true;
  }

  public async deleteFavorite(favoriteTypeArg: string, favoriteIdArg: string): Promise<boolean> {
    await this.request('/bha-api/favorites.cgi', {
      query: {
        action: 'remove',
        type: favoriteTypeArg,
        id: favoriteIdArg,
      },
    });
    return true;
  }

  public async getSchedule(): Promise<IDoorbirdScheduleEntry[]> {
    return this.requestJson<IDoorbirdScheduleEntry[]>('/bha-api/schedule.cgi');
  }

  public async changeSchedule(entryArg: IDoorbirdScheduleEntry): Promise<boolean> {
    await this.request('/bha-api/schedule.cgi', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(entryArg),
    });
    return true;
  }

  public async deleteSchedule(inputArg: string, paramArg = ''): Promise<boolean> {
    await this.request('/bha-api/schedule.cgi', {
      query: {
        action: 'remove',
        input: inputArg,
        param: paramArg,
      },
    });
    return true;
  }

  public async getSnapshotImage(cameraIdArg: string = 'live'): Promise<IDoorbirdSnapshotImage> {
    const cameraId = cameraIdArg as TDoorbirdCameraKind;
    const path = cameraId === 'last_ring'
      ? '/bha-api/history.cgi?index=1&event=doorbell'
      : cameraId === 'last_motion'
        ? '/bha-api/history.cgi?index=1&event=motionsensor'
        : '/bha-api/image.cgi';
    const response = await this.request(path, {}, this.config.snapshotTimeoutMs || doorbirdDefaultSnapshotTimeoutMs);
    return {
      contentType: response.headers.get('content-type') || 'image/jpeg',
      data: new Uint8Array(await response.arrayBuffer()),
    };
  }

  public async energizeRelay(relayArg = '1'): Promise<boolean> {
    const data = await this.requestJson<Record<string, unknown>>('/bha-api/open-door.cgi', { query: { r: relayArg } });
    return this.successFromReturnCode(data);
  }

  public async turnLightOn(): Promise<boolean> {
    const data = await this.requestJson<Record<string, unknown>>('/bha-api/light-on.cgi');
    return this.successFromReturnCode(data);
  }

  public async resetFavorites(): Promise<Array<{ type: string; id: string; success: boolean }>> {
    const favorites = await this.getFavorites();
    const results: Array<{ type: string; id: string; success: boolean }> = [];
    for (const [type, entries] of Object.entries(favorites)) {
      for (const id of Object.keys(entries || {})) {
        results.push({ type, id, success: await this.deleteFavorite(type, id) });
      }
    }
    return results;
  }

  public async execute(commandArg: IDoorbirdCommandRequest): Promise<unknown> {
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }
    if (this.config.client && !this.config.host) {
      return this.executeWithClient(this.config.client, commandArg);
    }
    if (commandArg.action === 'refresh') {
      return this.refresh();
    }
    if (commandArg.action === 'stream_source') {
      const snapshot = await this.getSnapshot();
      const camera = snapshot.cameras.find((cameraArg) => cameraArg.id === commandArg.cameraId) || snapshot.cameras[0];
      return {
        cameraId: camera?.id,
        stillImageUrl: camera?.stillImageUrl,
        liveVideoUrl: camera?.liveVideoUrl,
        rtspUrl: camera?.rtspUrl,
        requiresAuth: true,
      };
    }
    if (!this.config.host) {
      throw new DoorbirdApiConnectionError('DoorBird commands require config.host, config.client, or commandExecutor.');
    }
    if (commandArg.action === 'snapshot_image') {
      const image = await this.getSnapshotImage(commandArg.cameraId);
      return { contentType: image.contentType, dataBase64: Buffer.from(image.data).toString('base64') };
    }
    if (commandArg.action === 'energize_relay') {
      return { ok: await this.energizeRelay(commandArg.relay || '1'), relay: commandArg.relay || '1' };
    }
    if (commandArg.action === 'turn_light_on') {
      return { ok: await this.turnLightOn() };
    }
    if (commandArg.action === 'doorbell_state') {
      return { doorbell: await this.getDoorbellState() };
    }
    if (commandArg.action === 'motion_sensor_state') {
      return { motion: await this.getMotionSensorState() };
    }
    if (commandArg.action === 'favorites') {
      return this.getFavorites();
    }
    if (commandArg.action === 'change_favorite') {
      if (!commandArg.favorite) {
        throw new DoorbirdApiError('DoorBird change_favorite requires a favorite payload.');
      }
      return { ok: await this.changeFavorite(commandArg.favorite), favorite: commandArg.favorite };
    }
    if (commandArg.action === 'delete_favorite') {
      if (!commandArg.favoriteType || !commandArg.favoriteId) {
        throw new DoorbirdApiError('DoorBird delete_favorite requires favoriteType and favoriteId.');
      }
      return { ok: await this.deleteFavorite(commandArg.favoriteType, commandArg.favoriteId), type: commandArg.favoriteType, id: commandArg.favoriteId };
    }
    if (commandArg.action === 'schedule') {
      return this.getSchedule();
    }
    if (commandArg.action === 'change_schedule') {
      if (!commandArg.scheduleEntry) {
        throw new DoorbirdApiError('DoorBird change_schedule requires scheduleEntry.');
      }
      return { ok: await this.changeSchedule(commandArg.scheduleEntry), scheduleEntry: commandArg.scheduleEntry };
    }
    if (commandArg.action === 'delete_schedule') {
      if (!commandArg.scheduleInput) {
        throw new DoorbirdApiError('DoorBird delete_schedule requires scheduleInput.');
      }
      return { ok: await this.deleteSchedule(commandArg.scheduleInput, commandArg.scheduleParam), input: commandArg.scheduleInput, param: commandArg.scheduleParam };
    }
    if (commandArg.action === 'reset_favorites') {
      return this.resetFavorites();
    }
    throw new DoorbirdApiError(`Unsupported DoorBird command: ${commandArg.action}`);
  }

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

  public static async verifySupportedDevice(hostArg: string, portArg = doorbirdDefaultPort, timeoutMsArg = 3000): Promise<boolean> {
    const baseUrl = endpointBaseUrl({ host: hostArg, port: portArg });
    const url = `${baseUrl}/bha-api/monitor.cgi?check=doorbell`;
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), timeoutMsArg);
    try {
      const response = await globalThis.fetch(url, { signal: abortController.signal });
      await response.arrayBuffer().catch(() => undefined);
      return response.status === 401;
    } catch {
      return false;
    } finally {
      clearTimeout(timeout);
    }
  }

  private async executeWithClient(clientArg: IDoorbirdClientLike, commandArg: IDoorbirdCommandRequest): Promise<unknown> {
    if (commandArg.action === 'refresh') {
      return this.refresh();
    }
    if (commandArg.action === 'snapshot_image' && clientArg.getSnapshotImage) {
      const image = await clientArg.getSnapshotImage(commandArg.cameraId);
      return { contentType: image.contentType, dataBase64: Buffer.from(image.data).toString('base64') };
    }
    if (commandArg.action === 'energize_relay' && clientArg.energizeRelay) {
      return clientArg.energizeRelay(commandArg.relay || '1');
    }
    if (commandArg.action === 'turn_light_on' && clientArg.turnLightOn) {
      return clientArg.turnLightOn();
    }
    if (commandArg.action === 'doorbell_state' && clientArg.getDoorbellState) {
      return { doorbell: await clientArg.getDoorbellState() };
    }
    if (commandArg.action === 'motion_sensor_state' && clientArg.getMotionSensorState) {
      return { motion: await clientArg.getMotionSensorState() };
    }
    if (commandArg.action === 'favorites' && clientArg.favorites) {
      return clientArg.favorites();
    }
    if (commandArg.action === 'change_favorite' && commandArg.favorite && clientArg.changeFavorite) {
      return clientArg.changeFavorite(commandArg.favorite);
    }
    if (commandArg.action === 'delete_favorite' && commandArg.favoriteType && commandArg.favoriteId && clientArg.deleteFavorite) {
      return clientArg.deleteFavorite(commandArg.favoriteType, commandArg.favoriteId);
    }
    if (commandArg.action === 'schedule' && clientArg.schedule) {
      return clientArg.schedule();
    }
    if (commandArg.action === 'change_schedule' && commandArg.scheduleEntry && clientArg.changeSchedule) {
      return clientArg.changeSchedule(commandArg.scheduleEntry);
    }
    if (commandArg.action === 'delete_schedule' && commandArg.scheduleInput && clientArg.deleteSchedule) {
      return clientArg.deleteSchedule(commandArg.scheduleInput, commandArg.scheduleParam);
    }
    if (commandArg.action === 'reset_favorites' && clientArg.resetFavorites) {
      return clientArg.resetFavorites();
    }
    throw new DoorbirdApiConnectionError('DoorBird command is not available on the injected client and no HTTP endpoint or commandExecutor is configured.');
  }

  private async snapshotFromClient(clientArg: IDoorbirdClientLike): Promise<IDoorbirdSnapshot> {
    if (clientArg.getSnapshot) {
      const result = await clientArg.getSnapshot();
      if (isDoorbirdSnapshot(result)) {
        return DoorbirdMapper.toSnapshot({ config: { ...this.config, snapshot: result }, source: 'client', online: result.online });
      }
      return DoorbirdMapper.toSnapshot({ config: this.config, rawData: result, online: true, source: 'client' });
    }

    const [info, doorbell, motion, favorites, schedule] = await Promise.all([
      clientArg.getInfo ? clientArg.getInfo() : undefined,
      clientArg.getDoorbellState ? clientArg.getDoorbellState().catch(() => undefined) : undefined,
      clientArg.getMotionSensorState ? clientArg.getMotionSensorState().catch(() => undefined) : undefined,
      clientArg.favorites ? clientArg.favorites().catch(() => undefined) : undefined,
      clientArg.schedule ? clientArg.schedule().catch(() => undefined) : undefined,
    ]);
    if (!info && doorbell === undefined && motion === undefined && !favorites && !schedule) {
      throw new DoorbirdApiConnectionError('DoorBird client must expose getSnapshot() or at least one raw API getter.');
    }
    return DoorbirdMapper.toSnapshot({
      config: this.config,
      rawData: {
        info,
        status: { doorbell, motion, updatedAt: new Date().toISOString() },
        favorites,
        schedule,
      },
      online: true,
      source: 'client',
    });
  }

  private async monitorState(kindArg: 'doorbell' | 'motionsensor'): Promise<boolean> {
    const text = await (await this.request('/bha-api/monitor.cgi', { query: { check: kindArg } })).text();
    const match = text.match(/=\s*([01])/);
    return match ? match[1] === '1' : false;
  }

  private async requestJson<TValue>(pathArg: string, optionsArg: IDoorbirdRequestOptions = {}): Promise<TValue> {
    const response = await this.request(pathArg, { ...optionsArg, headers: { accept: 'application/json', ...optionsArg.headers } });
    const text = await response.text();
    if (!text.trim()) {
      return {} as TValue;
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new DoorbirdApiConnectionError(`Unable to parse DoorBird JSON from ${pathArg}: ${this.errorMessage(errorArg)}`);
    }
  }

  private async request(pathArg: string, optionsArg: IDoorbirdRequestOptions = {}, timeoutMsArg = this.config.timeoutMs || doorbirdDefaultTimeoutMs): Promise<Response> {
    if (!this.config.host) {
      throw new DoorbirdApiConnectionError('DoorBird HTTP requests require config.host.');
    }
    const url = this.url(pathArg, optionsArg.query);
    const headers = new Headers(optionsArg.headers);
    if (optionsArg.auth !== false && (this.config.username || this.config.password !== undefined)) {
      headers.set('authorization', this.basicAuthorization());
    }
    const response = await this.fetchWithTimeout(url, {
      method: optionsArg.method || 'GET',
      headers,
      body: optionsArg.body,
    }, timeoutMsArg);
    return this.checkedResponse(response, pathArg);
  }

  private async checkedResponse(responseArg: Response, pathArg: string): Promise<Response> {
    if (!responseArg.ok) {
      const text = await responseArg.text().catch(() => '');
      if (responseArg.status === 401) {
        throw new DoorbirdApiAuthorizationError('Please check your DoorBird credentials.');
      }
      if (responseArg.status === 404) {
        throw new DoorbirdApiNotFoundError(`DoorBird endpoint ${pathArg} was not found.`);
      }
      throw new DoorbirdApiConnectionError(`DoorBird endpoint ${pathArg} failed with HTTP ${responseArg.status}${text ? `: ${text}` : ''}`);
    }
    return responseArg;
  }

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit, timeoutMsArg: number): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), timeoutMsArg);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } catch (errorArg) {
      throw new DoorbirdApiConnectionError(`Connection to ${urlArg} failed: ${this.errorMessage(errorArg)}`);
    } finally {
      clearTimeout(timeout);
    }
  }

  private successFromReturnCode(dataArg: Record<string, unknown>): boolean {
    const bha = recordValue(dataArg.BHA);
    const code = bha?.RETURNCODE;
    return Number(code) === 1 || String(code) === '1';
  }

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

  private url(pathArg: string, queryArg?: Record<string, string | number | boolean | undefined>): string {
    if (/^https?:\/\//i.test(pathArg)) {
      return pathArg;
    }
    const [path, existingQuery] = pathArg.split('?');
    const params = new URLSearchParams(existingQuery || '');
    for (const [key, value] of Object.entries(queryArg || {})) {
      if (value !== undefined) {
        params.set(key, String(value));
      }
    }
    const query = params.toString();
    return `${this.baseUrl()}${path.startsWith('/') ? path : `/${path}`}${query ? `?${query}` : ''}`;
  }

  private baseUrl(): string {
    return endpointBaseUrl(this.config);
  }

  private hasManualData(): boolean {
    return Boolean(this.config.rawInfo || this.config.status || this.config.favorites || this.config.schedule || this.config.relays?.length);
  }

  private offlineSnapshot(errorArg: string): IDoorbirdSnapshot {
    return DoorbirdMapper.toSnapshot({
      config: this.config,
      online: false,
      source: 'runtime',
      error: errorArg,
    });
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }

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

interface IDoorbirdRequestOptions {
  method?: 'GET' | 'POST';
  query?: Record<string, string | number | boolean | undefined>;
  headers?: HeadersInit;
  body?: BodyInit;
  auth?: boolean;
}

const endpointBaseUrl = (configArg: Pick<IDoorbirdConfig, 'host' | 'port' | 'protocol' | 'ssl' | 'tls'>): string => {
  const host = configArg.host || 'localhost';
  if (/^https?:\/\//i.test(host)) {
    const url = new URL(host);
    return `${url.protocol}//${url.host}`;
  }
  const protocol: TDoorbirdProtocol = configArg.protocol || (configArg.ssl || configArg.tls ? 'https' : 'http');
  const defaultPort = protocol === 'https' ? 443 : doorbirdDefaultPort;
  const port = configArg.port && configArg.port !== defaultPort ? `:${configArg.port}` : '';
  return `${protocol}://${hostForUrl(host)}${port}`;
};

const hostForUrl = (hostArg: string): string => hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;

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

const isDoorbirdSnapshot = (valueArg: unknown): valueArg is IDoorbirdSnapshot => {
  return Boolean(valueArg && typeof valueArg === 'object' && 'deviceInfo' in valueArg && 'cameras' in valueArg && 'buttons' in valueArg);
};
