import type {
  INanoleafAuthTokenResult,
  INanoleafConfig,
  INanoleafControllerInfo,
  INanoleafEffects,
  INanoleafEffectsCommand,
  INanoleafPanelLayout,
  INanoleafRhythmInfo,
  INanoleafSnapshot,
  INanoleafState,
  INanoleafStateCommand,
} from './nanoleaf.types.js';

const DEFAULT_NANOLEAF_PORT = 16021;

export class NanoleafClient {
  constructor(private readonly config: INanoleafConfig) {}

  public async getSnapshot(): Promise<INanoleafSnapshot> {
    if (this.config.snapshot) {
      return this.normalizeSnapshot(this.config.snapshot);
    }

    if (this.canRequest()) {
      const controllerInfo = await this.requestJson<INanoleafControllerInfo>(this.authenticatedPath(''));
      return this.normalizeSnapshot({
        controllerInfo,
        state: controllerInfo.state ?? {},
        effects: controllerInfo.effects ?? {},
        panelLayout: controllerInfo.panelLayout,
        rhythm: controllerInfo.rhythm,
      });
    }

    return this.normalizeSnapshot({
      controllerInfo: this.config.controllerInfo ?? this.defaultControllerInfo(),
      state: this.config.state ?? this.config.controllerInfo?.state ?? {},
      effects: this.config.effects ?? this.config.controllerInfo?.effects ?? {},
      panelLayout: this.config.panelLayout ?? this.config.controllerInfo?.panelLayout,
      rhythm: this.config.rhythm ?? this.config.controllerInfo?.rhythm,
    });
  }

  public async getControllerInfo(): Promise<INanoleafControllerInfo> {
    return (await this.getSnapshot()).controllerInfo;
  }

  public async getState(): Promise<INanoleafState> {
    if (this.config.snapshot || this.config.state || this.config.controllerInfo?.state || !this.canRequest()) {
      return (await this.getSnapshot()).state;
    }
    return this.requestJson<INanoleafState>(this.authenticatedPath('/state'));
  }

  public async getEffects(): Promise<INanoleafEffects> {
    if (this.config.snapshot || this.config.effects || this.config.controllerInfo?.effects || !this.canRequest()) {
      return (await this.getSnapshot()).effects;
    }
    return this.requestJson<INanoleafEffects>(this.authenticatedPath('/effects'));
  }

  public async getPanelLayout(): Promise<INanoleafPanelLayout | undefined> {
    return (await this.getSnapshot()).panelLayout;
  }

  public async getRhythm(): Promise<INanoleafRhythmInfo | undefined> {
    return (await this.getSnapshot()).rhythm;
  }

  public async turnOn(): Promise<void> {
    await this.setState({ on: { value: true } });
  }

  public async turnOff(transitionArg?: number): Promise<void> {
    if (transitionArg === undefined) {
      await this.setState({ on: { value: false } });
      return;
    }
    await this.setState({ on: { value: false }, brightness: { value: 0, duration: transitionArg } });
  }

  public async setBrightness(percentArg: number, transitionArg?: number): Promise<void> {
    await this.setState({
      brightness: {
        value: this.clamp(Math.round(percentArg), 0, 100),
        duration: transitionArg,
      },
    });
  }

  public async setHue(hueArg: number): Promise<void> {
    await this.setState({ hue: { value: this.clamp(Math.round(hueArg), 0, 360) } });
  }

  public async setSaturation(saturationArg: number): Promise<void> {
    await this.setState({ sat: { value: this.clamp(Math.round(saturationArg), 0, 100) } });
  }

  public async setColorTemperature(kelvinArg: number): Promise<void> {
    await this.setState({ ct: { value: Math.round(kelvinArg) } });
  }

  public async setState(stateArg: INanoleafStateCommand): Promise<void> {
    this.applyStatePatch(stateArg);
    if (this.canRequest()) {
      await this.requestJson<void>(this.authenticatedPath('/state'), {
        method: 'PUT',
        body: stateArg,
      });
      return;
    }
    this.assertFixtureMode();
  }

  public async setEffect(effectArg: string): Promise<void> {
    this.applyEffectsPatch({ select: effectArg });
    if (this.canRequest()) {
      await this.requestJson<void>(this.authenticatedPath('/effects'), {
        method: 'PUT',
        body: { select: effectArg },
      });
      return;
    }
    this.assertFixtureMode();
  }

  public async writeEffectsCommand(commandArg: INanoleafEffectsCommand): Promise<void> {
    if (this.canRequest()) {
      await this.requestJson<void>(this.authenticatedPath('/effects'), {
        method: 'PUT',
        body: { write: commandArg },
      });
      return;
    }
    this.assertFixtureMode();
  }

  public async identify(): Promise<void> {
    if (this.canRequest()) {
      await this.requestJson<void>(this.authenticatedPath('/identify'), { method: 'PUT' });
      return;
    }
    this.assertFixtureMode();
  }

  public async createAuthToken(): Promise<INanoleafAuthTokenResult> {
    return {
      success: false,
      error: 'Nanoleaf pairing/token generation is not implemented. Put the controller in pairing mode and provide an existing authToken.',
    };
  }

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

  private normalizeSnapshot(snapshotArg: INanoleafSnapshot): INanoleafSnapshot {
    const controllerInfo = snapshotArg.controllerInfo;
    const state = snapshotArg.state ?? controllerInfo.state ?? {};
    const effects = snapshotArg.effects ?? controllerInfo.effects ?? {};
    const panelLayout = snapshotArg.panelLayout ?? controllerInfo.panelLayout;
    const rhythm = snapshotArg.rhythm ?? controllerInfo.rhythm;
    controllerInfo.state = state;
    controllerInfo.effects = effects;
    controllerInfo.panelLayout = panelLayout;
    controllerInfo.rhythm = rhythm;
    return { controllerInfo, state, effects, panelLayout, rhythm };
  }

  private applyStatePatch(stateArg: INanoleafStateCommand): void {
    const state = this.ensureMutableState();
    if (stateArg.on) {
      state.on = { ...state.on, ...stateArg.on };
    }
    if (stateArg.brightness) {
      state.brightness = { ...state.brightness, ...stateArg.brightness };
    }
    if (stateArg.hue) {
      state.hue = { ...state.hue, ...stateArg.hue };
    }
    if (stateArg.sat) {
      state.sat = { ...state.sat, ...stateArg.sat };
    }
    if (stateArg.ct) {
      state.ct = { ...state.ct, ...stateArg.ct };
    }
  }

  private applyEffectsPatch(effectsArg: Partial<INanoleafEffects>): void {
    const effects = this.ensureMutableEffects();
    Object.assign(effects, effectsArg);
  }

  private ensureMutableState(): INanoleafState {
    if (this.config.snapshot) {
      this.config.snapshot.state ||= {};
      this.config.snapshot.controllerInfo.state = this.config.snapshot.state;
      return this.config.snapshot.state;
    }
    this.config.state ||= this.config.controllerInfo?.state ?? {};
    if (this.config.controllerInfo) {
      this.config.controllerInfo.state = this.config.state;
    }
    return this.config.state;
  }

  private ensureMutableEffects(): INanoleafEffects {
    if (this.config.snapshot) {
      this.config.snapshot.effects ||= {};
      this.config.snapshot.controllerInfo.effects = this.config.snapshot.effects;
      return this.config.snapshot.effects;
    }
    this.config.effects ||= this.config.controllerInfo?.effects ?? {};
    if (this.config.controllerInfo) {
      this.config.controllerInfo.effects = this.config.effects;
    }
    return this.config.effects;
  }

  private assertFixtureMode(): void {
    if (!this.hasFixtureData()) {
      throw new Error('Nanoleaf host and authToken are required when snapshot/manual config data is not provided.');
    }
  }

  private hasFixtureData(): boolean {
    return Boolean(this.config.snapshot || this.config.controllerInfo || this.config.state || this.config.effects || this.config.panelLayout || this.config.rhythm);
  }

  private canRequest(): boolean {
    return Boolean(this.config.host && this.authToken());
  }

  private authToken(): string | undefined {
    return this.config.authToken || this.config.token;
  }

  private authenticatedPath(pathArg: string): string {
    const token = this.authToken();
    if (!token) {
      throw new Error('Nanoleaf authToken is required for local HTTP requests.');
    }
    return `/api/v1/${encodeURIComponent(token)}${pathArg}`;
  }

  private async requestJson<TResult>(pathArg: string, optionsArg: { method?: string; body?: unknown } = {}): Promise<TResult> {
    const response = await globalThis.fetch(`${this.baseUrl()}${pathArg}`, {
      method: optionsArg.method ?? (optionsArg.body === undefined ? 'GET' : 'POST'),
      headers: optionsArg.body === undefined ? undefined : { 'content-type': 'application/json' },
      body: optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body),
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`Nanoleaf request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    if (!text) {
      return undefined as TResult;
    }
    return JSON.parse(text) as TResult;
  }

  private baseUrl(): string {
    if (!this.config.host) {
      throw new Error('Nanoleaf host is required for local HTTP requests.');
    }
    const protocol = this.config.protocol ?? 'http';
    const port = this.config.port ?? DEFAULT_NANOLEAF_PORT;
    return `${protocol}://${this.config.host}:${port}`;
  }

  private defaultControllerInfo(): INanoleafControllerInfo {
    return {
      name: 'Nanoleaf',
      manufacturer: 'Nanoleaf',
      model: 'Nanoleaf Controller',
    };
  }

  private clamp(valueArg: number, minArg: number, maxArg: number): number {
    return Math.max(minArg, Math.min(maxArg, valueArg));
  }
}
