import type {
  IDeconzConfig,
  IDeconzGatewayConfig,
  IDeconzGroupActionPatch,
  IDeconzLightStatePatch,
  IDeconzSensorConfigPatch,
  IDeconzSensorStatePatch,
  IDeconzSnapshot,
  IDeconzWebsocketEvent,
} from './deconz.types.js';

type TWebSocketMessage = { data: unknown };

type TWebSocketLike = {
  close(): void;
  addEventListener?: (eventArg: 'message', handlerArg: (messageArg: TWebSocketMessage) => void) => void;
  onmessage?: (messageArg: TWebSocketMessage) => void;
};

type TWebSocketConstructor = new (urlArg: string) => TWebSocketLike;

export class DeconzClient {
  constructor(private readonly config: IDeconzConfig) {}

  public async getSnapshot(): Promise<IDeconzSnapshot> {
    if (this.config.snapshot) {
      return this.config.snapshot;
    }
    return this.requestJson<IDeconzSnapshot>('GET', '');
  }

  public async getGatewayConfig(): Promise<IDeconzGatewayConfig> {
    if (this.config.snapshot?.config) {
      return this.config.snapshot.config;
    }
    return this.requestJson<IDeconzGatewayConfig>('GET', '/config');
  }

  public async setLightState(lightIdArg: string, stateArg: IDeconzLightStatePatch): Promise<void> {
    if (this.canUseHttp()) {
      await this.put(`/lights/${encodeURIComponent(lightIdArg)}/state`, stateArg);
    }
    this.applyLightState(lightIdArg, stateArg);
  }

  public async setGroupState(groupIdArg: string, actionArg: IDeconzGroupActionPatch): Promise<void> {
    if (this.canUseHttp()) {
      await this.put(`/groups/${encodeURIComponent(groupIdArg)}/action`, actionArg);
    }
    this.applyGroupState(groupIdArg, actionArg);
  }

  public async setSensorConfig(sensorIdArg: string, configArg: IDeconzSensorConfigPatch): Promise<void> {
    if (this.canUseHttp()) {
      await this.put(`/sensors/${encodeURIComponent(sensorIdArg)}/config`, configArg);
    }
    this.applySensorConfig(sensorIdArg, configArg);
  }

  public async setSensorState(sensorIdArg: string, stateArg: IDeconzSensorStatePatch): Promise<void> {
    if (this.canUseHttp()) {
      await this.put(`/sensors/${encodeURIComponent(sensorIdArg)}/state`, stateArg);
    }
    this.applySensorState(sensorIdArg, stateArg);
  }

  public async recallScene(groupIdArg: string, sceneIdArg: string): Promise<void> {
    if (this.canUseHttp()) {
      await this.put(`/groups/${encodeURIComponent(groupIdArg)}/scenes/${encodeURIComponent(sceneIdArg)}/recall`, {});
    }
    const group = this.config.snapshot?.groups[groupIdArg];
    if (group) {
      group.action = { ...group.action, scene: sceneIdArg };
    }
  }

  public async put<TResult = unknown>(pathArg: string, dataArg: Record<string, unknown>): Promise<TResult> {
    return this.requestJson<TResult>('PUT', pathArg, dataArg);
  }

  public async post<TResult = unknown>(pathArg: string, dataArg: Record<string, unknown>): Promise<TResult> {
    return this.requestJson<TResult>('POST', pathArg, dataArg);
  }

  public async subscribeToEvents(handlerArg: (eventArg: IDeconzWebsocketEvent) => void): Promise<() => Promise<void>> {
    if (this.config.enableWebSocket === false || !this.config.host) {
      return async () => {};
    }

    const WebSocketCtor = (globalThis as typeof globalThis & { WebSocket?: TWebSocketConstructor }).WebSocket;
    if (!WebSocketCtor) {
      return async () => {};
    }

    const websocketPort = await this.resolveWebSocketPort();
    if (!websocketPort) {
      return async () => {};
    }

    const socket = new WebSocketCtor(`ws://${this.config.host}:${websocketPort}`);
    const listener = (messageArg: TWebSocketMessage) => {
      const event = this.parseWebSocketMessage(messageArg.data);
      if (event) {
        handlerArg(event);
      }
    };

    if (socket.addEventListener) {
      socket.addEventListener('message', listener);
    } else {
      socket.onmessage = listener;
    }

    return async () => {
      socket.close();
    };
  }

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

  private async requestJson<TResult>(methodArg: string, pathArg: string, dataArg?: Record<string, unknown>): Promise<TResult> {
    if (!this.canUseHttp()) {
      throw new Error('deCONZ host and apiKey are required when snapshot data is not provided.');
    }

    const response = await globalThis.fetch(`${this.baseUrl()}${this.apiPath(pathArg)}`, {
      method: methodArg,
      headers: dataArg ? { 'content-type': 'application/json' } : undefined,
      body: dataArg ? JSON.stringify(dataArg) : undefined,
    });
    const text = await response.text();
    const parsed = text ? JSON.parse(text) as unknown : undefined;

    if (!response.ok) {
      throw new Error(`deCONZ request ${methodArg} ${pathArg || '/'} failed with HTTP ${response.status}: ${text}`);
    }

    const apiError = this.findApiError(parsed);
    if (apiError) {
      throw new Error(`deCONZ request ${methodArg} ${pathArg || '/'} failed: ${apiError}`);
    }

    return parsed as TResult;
  }

  private baseUrl(): string {
    const protocol = this.config.protocol || 'http';
    const port = this.config.port ?? (protocol === 'https' ? 443 : 80);
    const defaultPort = protocol === 'https' ? 443 : 80;
    return `${protocol}://${this.config.host}${port === defaultPort ? '' : `:${port}`}`;
  }

  private apiPath(pathArg: string): string {
    const suffix = pathArg.startsWith('/') ? pathArg : pathArg ? `/${pathArg}` : '';
    return `/api/${encodeURIComponent(String(this.config.apiKey))}${suffix}`;
  }

  private canUseHttp(): boolean {
    return Boolean(this.config.host && this.config.apiKey);
  }

  private async resolveWebSocketPort(): Promise<number | undefined> {
    if (this.config.websocketPort) {
      return this.config.websocketPort;
    }
    if (this.config.snapshot?.config?.websocketport) {
      return this.config.snapshot.config.websocketport;
    }
    if (!this.canUseHttp()) {
      return undefined;
    }
    return (await this.getGatewayConfig()).websocketport;
  }

  private parseWebSocketMessage(dataArg: unknown): IDeconzWebsocketEvent | undefined {
    if (typeof dataArg === 'string') {
      return JSON.parse(dataArg) as IDeconzWebsocketEvent;
    }
    if (this.isRecord(dataArg)) {
      return dataArg as IDeconzWebsocketEvent;
    }
    return undefined;
  }

  private applyLightState(lightIdArg: string, stateArg: IDeconzLightStatePatch): void {
    const light = this.config.snapshot?.lights[lightIdArg];
    if (light) {
      light.state = { ...light.state, ...stateArg };
    }
  }

  private applyGroupState(groupIdArg: string, actionArg: IDeconzGroupActionPatch): void {
    const group = this.config.snapshot?.groups[groupIdArg];
    if (!group) {
      return;
    }
    group.action = { ...group.action, ...actionArg };
    if (typeof actionArg.on === 'boolean') {
      group.state = {
        ...group.state,
        any_on: actionArg.on,
        all_on: actionArg.on,
      };
    }
  }

  private applySensorConfig(sensorIdArg: string, configArg: IDeconzSensorConfigPatch): void {
    const sensor = this.config.snapshot?.sensors[sensorIdArg];
    if (sensor) {
      sensor.config = { ...sensor.config, ...configArg };
    }
  }

  private applySensorState(sensorIdArg: string, stateArg: IDeconzSensorStatePatch): void {
    const sensor = this.config.snapshot?.sensors[sensorIdArg];
    if (sensor) {
      sensor.state = { ...sensor.state, ...stateArg };
    }
  }

  private findApiError(valueArg: unknown): string | undefined {
    if (!Array.isArray(valueArg)) {
      return undefined;
    }
    for (const item of valueArg) {
      if (!this.isRecord(item) || !this.isRecord(item.error)) {
        continue;
      }
      const description = item.error.description;
      return typeof description === 'string' ? description : JSON.stringify(item.error);
    }
    return undefined;
  }

  private isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
    return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
  }
}
