import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { NrgkickMapper } from './nrgkick.mapper.js';
import type { INrgkickApiData, INrgkickConfig } from './nrgkick.types.js';
import { nrgkickDefaultTimeoutMs, nrgkickProfile } from './nrgkick.types.js';

export class NrgkickApiError extends Error {}
export class NrgkickApiConnectionError extends NrgkickApiError {}
export class NrgkickApiAuthenticationError extends NrgkickApiError {}
export class NrgkickApiDisabledError extends NrgkickApiError {}
export class NrgkickApiRejectedCommandError extends NrgkickApiError {}

export class NrgkickClient extends SimpleLocalClient<INrgkickConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(configArg: INrgkickConfig) {
    super(nrgkickProfile, configArg);
    this.configArg = configArg;
  }

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

    if (!forceRefreshArg && (this.configArg.client || this.configArg.snapshotProvider || this.hasManualData())) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return this.clone(this.cachedSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        this.cachedSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.cachedSnapshot = this.offlineNrgkickSnapshot(this.errorMessage(errorArg));
      }
      return this.clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return this.clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(Boolean(this.hasEndpoint()));
    const success = snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error || 'NRGkick refresh requires a live host/url, injected client, snapshotProvider, snapshot, or rawData.', data: snapshot };
  }

  public async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const rawData = await this.fetchRawData();
    return NrgkickMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.hasEndpoint()) {
      return super.execute(requestArg);
    }

    try {
      const data = await this.executeNative(requestArg);
      this.cachedSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

  public async setCurrent(currentArg: number): Promise<Record<string, unknown>> {
    return this.setControlValue('current_set', currentArg, currentArg, 0.1);
  }

  public async setChargePause(pauseArg: boolean): Promise<Record<string, unknown>> {
    return this.setControlValue('charge_pause', pauseArg ? '1' : '0', pauseArg ? 1 : 0);
  }

  public async setEnergyLimit(limitArg: number): Promise<Record<string, unknown>> {
    return this.setControlValue('energy_limit', Math.trunc(limitArg), Math.trunc(limitArg));
  }

  public async setPhaseCount(phasesArg: number): Promise<Record<string, unknown>> {
    return this.setControlValue('phase_count', Math.trunc(phasesArg), Math.trunc(phasesArg));
  }

  private readonly configArg: INrgkickConfig;

  private async fetchRawData(): Promise<INrgkickApiData> {
    const [info, control, values] = await Promise.all([
      this.requestJson('/info', { raw: '1' }),
      this.requestJson('/control'),
      this.requestJson('/values', { raw: '1' }),
    ]);
    return { info, control, values, fetchedAt: new Date().toISOString() };
  }

  private async executeNative(requestArg: IServiceCallRequest): Promise<unknown> {
    if (requestArg.service === 'turn_on') {
      return this.setChargePause(false);
    }
    if (requestArg.service === 'turn_off') {
      return this.setChargePause(true);
    }
    if (requestArg.service === 'toggle') {
      const control = await this.requestJson('/control');
      return this.setChargePause(!this.booleanValue(control.charge_pause));
    }
    if (requestArg.service === 'set_value') {
      const value = this.numberValue(requestArg.data?.value ?? requestArg.data?.native_value);
      if (value === undefined) {
        throw new NrgkickApiRejectedCommandError('NRGkick set_value requires data.value.');
      }
      const key = this.controlKey(requestArg);
      if (key === 'current_set') {
        return this.setCurrent(value);
      }
      if (key === 'energy_limit') {
        return this.setEnergyLimit(value);
      }
      if (key === 'phase_count') {
        return this.setPhaseCount(value);
      }
      throw new NrgkickApiRejectedCommandError(`Unsupported NRGkick number target for set_value: ${key || 'unknown'}.`);
    }
    throw new NrgkickApiRejectedCommandError(`Unsupported NRGkick service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async setControlValue(keyArg: 'current_set' | 'charge_pause' | 'energy_limit' | 'phase_count', valueArg: string | number, expectedArg: number, toleranceArg = 0): Promise<Record<string, unknown>> {
    const data = await this.requestJson('/control', { [keyArg]: String(valueArg) });
    const actual = this.numberValue(data[keyArg]);
    if (actual === undefined) {
      const reason = typeof data.Response === 'string' ? data.Response : `missing ${keyArg} in command response`;
      throw new NrgkickApiRejectedCommandError(reason);
    }
    if (Math.abs(actual - expectedArg) > toleranceArg) {
      throw new NrgkickApiRejectedCommandError(`NRGkick rejected ${keyArg}: expected ${expectedArg}, got ${actual}.`);
    }
    return data;
  }

  private async requestJson(pathArg: string, paramsArg: Record<string, string> = {}): Promise<Record<string, unknown>> {
    const url = new URL(`${this.baseUrl()}${pathArg}`);
    for (const [key, value] of Object.entries(paramsArg)) {
      url.searchParams.set(key, value);
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.configArg.timeoutMs || nrgkickDefaultTimeoutMs);
    try {
      const response = await globalThis.fetch(url, {
        method: 'GET',
        headers: this.headers(),
        signal: controller.signal,
      });
      const text = await response.text();
      const data = this.parseJson(text, url.toString());
      if (response.status === 401 || response.status === 403) {
        throw new NrgkickApiAuthenticationError(`NRGkick authentication failed with HTTP ${response.status}.`);
      }
      if (data.Response === 'API must be enabled within the NRGkick App') {
        throw new NrgkickApiDisabledError(data.Response);
      }
      if (!response.ok && typeof data.Response === 'string' && data.Response) {
        throw new NrgkickApiRejectedCommandError(data.Response);
      }
      if (!response.ok) {
        throw new NrgkickApiConnectionError(`NRGkick endpoint ${url.toString()} failed with HTTP ${response.status}${text ? `: ${text.slice(0, 160)}` : ''}`);
      }
      return data;
    } catch (errorArg) {
      if (errorArg instanceof NrgkickApiError) {
        throw errorArg;
      }
      throw new NrgkickApiConnectionError(`Connection to ${url.toString()} failed: ${this.errorMessage(errorArg)}`);
    } finally {
      clearTimeout(timeout);
    }
  }

  private baseUrl(): string {
    if (this.configArg.url) {
      const url = new URL(this.configArg.url.includes('://') ? this.configArg.url : `http://${this.configArg.url}`);
      return url.origin;
    }
    const host = this.configArg.host;
    if (!host) {
      throw new NrgkickApiConnectionError('NRGkick local JSON API requires config.host or config.url.');
    }
    const parsed = host.includes('://') ? new URL(host) : undefined;
    if (parsed) {
      return parsed.origin;
    }
    const protocol = this.configArg.ssl || this.configArg.useTls ? 'https' : 'http';
    const port = this.configArg.port ? `:${this.configArg.port}` : '';
    return `${protocol}://${this.hostForUrl(host)}${port}`;
  }

  private headers(): Record<string, string> {
    const headers: Record<string, string> = { accept: 'application/json' };
    if (this.configArg.username || this.configArg.password) {
      headers.authorization = `Basic ${Buffer.from(`${this.configArg.username || ''}:${this.configArg.password || ''}`).toString('base64')}`;
    }
    return headers;
  }

  private controlKey(requestArg: IServiceCallRequest): 'current_set' | 'energy_limit' | 'phase_count' | undefined {
    const configured = requestArg.data?.key || requestArg.data?.controlKey;
    if (configured === 'current_set' || configured === 'energy_limit' || configured === 'phase_count') {
      return configured;
    }
    const target = `${requestArg.target.entityId || ''} ${requestArg.target.deviceId || ''}`.toLowerCase();
    if (target.includes('energy_limit')) {
      return 'energy_limit';
    }
    if (target.includes('phase_count')) {
      return 'phase_count';
    }
    if (target.includes('current') || target.includes('current_set') || target.includes('charging_current')) {
      return 'current_set';
    }
    return undefined;
  }

  private parseJson(textArg: string, urlArg: string): Record<string, unknown> {
    try {
      const parsed = textArg ? JSON.parse(textArg) as unknown : {};
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error(`unexpected ${typeof parsed}`);
      }
      return parsed as Record<string, unknown>;
    } catch (errorArg) {
      throw new NrgkickApiConnectionError(`Invalid JSON response from ${urlArg}: ${this.errorMessage(errorArg)}`);
    }
  }

  private hasEndpoint(): boolean {
    return Boolean(this.configArg.host || this.configArg.url);
  }

  private hasManualData(): boolean {
    return Boolean(this.configArg.snapshot || this.configArg.rawData || this.configArg.rawText || this.configArg.entities);
  }

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

  private numberValue(valueArg: unknown): number | undefined {
    if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
      return valueArg;
    }
    if (typeof valueArg === 'string' && valueArg.trim()) {
      const parsed = Number(valueArg);
      return Number.isFinite(parsed) ? parsed : undefined;
    }
    return undefined;
  }

  private booleanValue(valueArg: unknown): boolean {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'number') {
      return valueArg !== 0;
    }
    if (typeof valueArg === 'string') {
      return valueArg === '1' || valueArg.toLowerCase() === 'true';
    }
    return false;
  }

  private offlineNrgkickSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return NrgkickMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorArg });
  }

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

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