import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IOemConfig } from './oem.types.js';
import { oemDefaultName, oemProfile } from './oem.types.js';

export class OemClient extends SimpleLocalClient<IOemConfig> {
  private currentOemSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IOemConfig) {
    super(oemProfile, configArg);
  }

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

    if (this.canUseNativeHttp()) {
      try {
        this.currentOemSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentOemSnapshot = this.offlineOemSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentOemSnapshot);
    }

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

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

    try {
      if (requestArg.service === 'set_temperature') {
        const temperature = numberValue(requestArg.data?.temperature ?? requestArg.data?.target_temperature ?? requestArg.data?.targetTemperature);
        if (temperature === undefined) {
          throw new Error('OpenEnergyMonitor thermostat set_temperature requires data.temperature.');
        }
        await this.postText('control/thermostat.cgi', { param: 'thermostat_manualsetpoint' }, String(Math.round(temperature * 100)));
        this.currentOemSnapshot = undefined;
        return { success: true, data: { temperature } };
      }

      if (requestArg.service === 'set_hvac_mode') {
        const mode = hvacModeToThermostatState(requestArg.data?.hvac_mode ?? requestArg.data?.hvacMode ?? requestArg.data?.mode);
        await this.postText('control/thermostat.cgi', { param: 'thermostat_state' }, String(mode));
        this.currentOemSnapshot = undefined;
        return { success: true, data: { mode, hvac_mode: thermostatStateToHvacMode(mode) } };
      }

      return { success: false, error: `Unsupported OpenEnergyMonitor thermostat service: ${requestArg.service}` };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const status = await this.getJson<Record<string, unknown>>('control/thermostat.cgi', { param: 'state' });
    const mode = numberValue(status.state);
    const relayOn = boolValue(status.relay1state);
    const currentTemperature = numberValue(status.temperature);
    const targetTemperature = numberValue(status.manualsetpoint) !== undefined ? numberValue(status.manualsetpoint)! / 100 : undefined;
    const hvacMode = thermostatStateToHvacMode(mode);
    const entity: ISimpleLocalEntitySnapshot = {
      id: 'thermostat',
      name: this.configArg.name || oemDefaultName,
      platform: 'climate',
      state: hvacMode,
      available: true,
      writable: true,
      unit: 'C',
      attributes: {
        current_temperature: currentTemperature,
        target_temperature: targetTemperature,
        hvac_action: !mode ? 'off' : relayOn ? 'heating' : 'idle',
        relay1state: relayOn,
        mode,
        supported_hvac_modes: ['auto', 'heat', 'off'],
      },
    };

    return {
      device: {
        id: this.configArg.uniqueId || this.deviceId(),
        name: this.configArg.name || oemDefaultName,
        manufacturer: oemProfile.manufacturer,
        model: oemProfile.model,
        host: this.endpoint().host,
        port: this.endpoint().port,
        protocol: this.endpoint().protocol,
        configurationUrl: this.baseUrl(),
        attributes: this.configArg.metadata,
      },
      entities: [entity],
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: status,
      capabilities: {
        localRead: true,
        localControl: true,
        liveControlRequiresExecutor: false,
      },
    };
  }

  private async getJson<TValue>(pathArg: string, paramsArg: Record<string, string>): Promise<TValue> {
    const response = await this.fetchWithTimeout(this.url(pathArg, paramsArg), { method: 'GET', headers: this.headers() });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`OpenEnergyMonitor thermostat ${pathArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    return JSON.parse(text) as TValue;
  }

  private async postText(pathArg: string, paramsArg: Record<string, string>, bodyArg: string): Promise<string> {
    const response = await this.fetchWithTimeout(this.url(pathArg, paramsArg), { method: 'POST', headers: this.headers(), body: bodyArg });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`OpenEnergyMonitor thermostat ${pathArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    return text;
  }

  private async fetchWithTimeout(urlArg: URL, initArg: RequestInit): Promise<Response> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.configArg.timeoutMs || 5000);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: controller.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private url(pathArg: string, paramsArg: Record<string, string>): URL {
    const url = new URL(`${this.baseUrl()}/${pathArg.replace(/^\/+/, '')}`);
    for (const [key, value] of Object.entries(paramsArg)) {
      url.searchParams.set(key, value);
    }
    return url;
  }

  private headers(): Record<string, string> {
    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')}`;
    }
    return headers;
  }

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

  private baseUrl(): string {
    const endpoint = this.endpoint();
    const portPart = endpoint.port && !((endpoint.protocol === 'https' && endpoint.port === 443) || (endpoint.protocol === 'http' && endpoint.port === 80)) ? `:${endpoint.port}` : '';
    return `${endpoint.protocol}://${endpoint.host}${portPart}${endpoint.path}`.replace(/\/+$/, '');
  }

  private endpoint(): { host: string; port?: number; protocol: 'http' | 'https'; path: string } {
    const value = this.configArg.url || this.configArg.host;
    if (!value) {
      throw new Error('OpenEnergyMonitor thermostat native HTTP requires config.url or config.host.');
    }
    const protocol = this.configArg.useTls ? 'https' : 'http';
    const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `${protocol}://${value}`);
    if (this.configArg.port) {
      url.port = String(this.configArg.port);
    }
    const protocolValue = url.protocol === 'https:' ? 'https' : 'http';
    return {
      host: url.hostname,
      port: url.port ? Number(url.port) : this.configArg.port || (protocolValue === 'https' ? 443 : 80),
      protocol: protocolValue,
      path: url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, ''),
    };
  }

  private deviceId(): string {
    const endpoint = this.endpoint();
    return `${endpoint.host}:${endpoint.port || 80}`;
  }

  private offlineOemSnapshot(errorArg: string): ISimpleLocalSnapshot {
    const endpoint = this.canUseNativeHttp() ? this.endpoint() : undefined;
    return {
      device: {
        id: this.configArg.uniqueId || (endpoint ? `${endpoint.host}:${endpoint.port || 80}` : this.configArg.name || oemProfile.domain),
        name: this.configArg.name || oemDefaultName,
        manufacturer: oemProfile.manufacturer,
        model: oemProfile.model,
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: endpoint?.protocol || 'http',
        configurationUrl: endpoint ? this.baseUrl() : undefined,
      },
      entities: [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: false, available: true, attributes: { source: 'runtime' } }],
      online: false,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    };
  }
}

const hvacModeToThermostatState = (valueArg: unknown): number => {
  const numeric = numberValue(valueArg);
  if (numeric !== undefined && numeric >= 0 && numeric <= 2) {
    return numeric;
  }
  const value = stringValue(valueArg)?.toLowerCase();
  if (value === 'auto') {
    return 1;
  }
  if (value === 'heat') {
    return 2;
  }
  if (value === 'off') {
    return 0;
  }
  throw new Error('OpenEnergyMonitor thermostat set_hvac_mode requires hvac_mode auto, heat, or off.');
};

const thermostatStateToHvacMode = (valueArg: unknown): 'auto' | 'heat' | 'off' => {
  const value = numberValue(valueArg);
  if (value === 1) {
    return 'auto';
  }
  if (value === 2) {
    return 'heat';
  }
  return 'off';
};

const boolValue = (valueArg: unknown): boolean => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'number') {
    return valueArg !== 0;
  }
  if (typeof valueArg === 'string') {
    return valueArg !== '0' && valueArg.toLowerCase() !== 'false';
  }
  return false;
};

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

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

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);
