import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import { KankunMapper } from './kankun.mapper.js';
import type { IKankunConfig } from './kankun.types.js';
import { kankunProfile } from './kankun.types.js';

export class KankunClientError extends Error {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'KankunClientError';
  }
}

interface IKankunEndpoint {
  protocol: 'http' | 'https';
  host: string;
  port?: number;
  path: string;
}

export class KankunClient extends SimpleLocalClient<IKankunConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IKankunConfig) {
    super(kankunProfile, configArg);
  }

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

    if (this.configArg.client || this.configArg.snapshotProvider || (!forceRefreshArg && this.configArg.snapshot)) {
      return super.getSnapshot(forceRefreshArg);
    }

    if (this.endpoint() && this.configArg.transport !== 'snapshot') {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
        return clone(this.liveSnapshot);
      } catch (errorArg) {
        this.liveSnapshot = KankunMapper.toSnapshot({
          config: this.configArg,
          online: false,
          source: 'runtime',
          error: errorMessage(errorArg),
        });
        return clone(this.liveSnapshot);
      }
    }

    return super.getSnapshot(forceRefreshArg);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.liveSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

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

    if (!this.endpoint() || this.configArg.transport === 'snapshot') {
      return super.execute(requestArg);
    }

    try {
      let nextState: boolean;
      if (requestArg.service === 'turn_on') {
        nextState = true;
      } else if (requestArg.service === 'turn_off') {
        nextState = false;
      } else if (requestArg.service === 'toggle') {
        nextState = !(await this.fetchState());
      } else {
        return { success: false, error: `Unsupported Kankun service: ${requestArg.service}` };
      }

      const response = await this.requestJson({ set: nextState ? 'on' : 'off' });
      if (!response.ok) {
        throw new KankunClientError('Kankun json.cgi did not acknowledge the requested switch state.');
      }
      this.liveSnapshot = undefined;
      return { success: true, data: { state: nextState ? 'on' : 'off', response } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const isOn = await this.fetchState();
    const endpoint = this.endpoint();
    const host = endpoint?.host || this.configArg.host;
    const port = endpoint?.port || this.configArg.port || kankunProfile.defaultPort;
    const rawData = {
      device: {
        id: this.configArg.uniqueId || this.configArg.deviceId || (host ? `${host}:${port || ''}` : undefined),
        name: this.configArg.name || (host ? `Kankun ${host}` : kankunProfile.defaultName),
        manufacturer: kankunProfile.manufacturer,
        model: kankunProfile.model,
        host,
        port,
        protocol: endpoint?.protocol || 'http',
        configurationUrl: host ? `${endpoint?.protocol || 'http'}://${host}${port && port !== 80 ? `:${port}` : ''}` : undefined,
      },
      state: isOn ? 'on' : 'off',
      entities: [{
        id: 'switch',
        uniqueId: `${this.configArg.uniqueId || host || 'kankun'}_switch`,
        name: this.configArg.name || 'Kankun Switch',
        platform: 'switch' as const,
        state: isOn,
        available: true,
        writable: true,
        attributes: {
          path: endpoint?.path,
        },
      }],
      fetchedAt: new Date().toISOString(),
    };

    return KankunMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
  }

  private async fetchState(): Promise<boolean> {
    const response = await this.requestJson({ get: 'state' });
    if (response.state === 'on' || response.state === true) {
      return true;
    }
    if (response.state === 'off' || response.state === false) {
      return false;
    }
    throw new KankunClientError('Kankun json.cgi response did not contain state "on" or "off".');
  }

  private async requestJson(queryArg: Record<string, string>): Promise<Record<string, unknown>> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new KankunClientError('Kankun HTTP requests require config.host or config.url.');
    }
    if (endpoint.protocol !== 'http') {
      throw new KankunClientError('Kankun HTTPS/TLS is not implemented: Home Assistant kankun builds http:// json.cgi URLs only.');
    }

    const port = endpoint.port && endpoint.port !== 80 ? `:${endpoint.port}` : '';
    const path = endpoint.path.startsWith('/') ? endpoint.path : `/${endpoint.path}`;
    const url = new URL(`${endpoint.protocol}://${endpoint.host}${port}${path}`);
    for (const [key, value] of Object.entries(queryArg)) {
      url.searchParams.set(key, value);
    }

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeoutMs());
    try {
      const headers: Record<string, string> = { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' };
      if (this.configArg.username) {
        headers.authorization = `Basic ${Buffer.from(`${this.configArg.username}:${this.configArg.password || ''}`).toString('base64')}`;
      }
      const response = await globalThis.fetch(url, { headers, signal: controller.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new KankunClientError(`Kankun HTTP ${url.pathname} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      const parsed = JSON.parse(text) as unknown;
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new KankunClientError('Kankun json.cgi response was not a JSON object.');
      }
      return parsed as Record<string, unknown>;
    } finally {
      clearTimeout(timer);
    }
  }

  private endpoint(): IKankunEndpoint | undefined {
    if (this.configArg.url) {
      try {
        const url = new URL(this.configArg.url);
        if (url.protocol !== 'http:' && url.protocol !== 'https:') {
          return undefined;
        }
        return {
          protocol: url.protocol === 'https:' ? 'https' : 'http',
          host: url.hostname,
          port: url.port ? Number(url.port) : undefined,
          path: url.pathname || kankunProfile.defaultHttpPath || '/cgi-bin/json.cgi',
        };
      } catch {
        return undefined;
      }
    }
    if (!this.configArg.host) {
      return undefined;
    }
    return {
      protocol: this.configArg.useTls ? 'https' : 'http',
      host: this.configArg.host,
      port: this.configArg.port || kankunProfile.defaultPort,
      path: this.configArg.path || kankunProfile.defaultHttpPath || '/cgi-bin/json.cgi',
    };
  }

  private timeoutMs(): number {
    return this.configArg.timeoutMs || 5000;
  }
}

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
