import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { IntellifireMapper } from './intellifire.mapper.js';
import type { IIntellifireConfig, IIntellifirePollData, TIntellifireRawData } from './intellifire.types.js';
import { intellifireDefaultPort, intellifireDefaultTimeoutMs, intellifireProfile } from './intellifire.types.js';

type TIntellifireLocalCommand = 'power' | 'pilot' | 'light' | 'flame_height' | 'fan_speed' | 'thermostat_setpoint' | 'time_remaining' | 'beep' | 'soft_reset';

interface IIntellifireCommandSpec {
  command: TIntellifireLocalCommand;
  value: number;
}

const commandRanges: Record<TIntellifireLocalCommand, { min: number; max: number }> = {
  power: { min: 0, max: 1 },
  pilot: { min: 0, max: 1 },
  light: { min: 0, max: 3 },
  flame_height: { min: 0, max: 4 },
  fan_speed: { min: 0, max: 4 },
  thermostat_setpoint: { min: 0, max: 3700 },
  time_remaining: { min: 0, max: 10800 },
  beep: { min: 1, max: 1 },
  soft_reset: { min: 1, max: 1 },
};

export class IntellifireClient extends SimpleLocalClient<IIntellifireConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly clientConfig: IIntellifireConfig) {
    super(intellifireProfile, clientConfig);
  }

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

    if (!forceRefreshArg && this.clientConfig.snapshot) {
      this.cachedSnapshot = IntellifireMapper.toSnapshot({ config: this.clientConfig, source: 'snapshot', online: this.clientConfig.snapshot.online });
      return clone(this.cachedSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        const poll = await this.fetchPollData();
        this.cachedSnapshot = IntellifireMapper.toSnapshot({ config: this.clientConfig, rawData: { poll, fetchedAt: new Date().toISOString() }, online: true, source: 'http' });
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.cachedSnapshot);
    }

    if (this.clientConfig.client || this.clientConfig.snapshotProvider) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }

    if (this.hasManualData()) {
      this.cachedSnapshot = IntellifireMapper.toSnapshot({ config: this.clientConfig, rawData: this.rawData(), online: this.clientConfig.online ?? true, source: 'manual' });
      return clone(this.cachedSnapshot);
    }

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

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

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.clientConfig.client?.execute) {
      return normalizeServiceResult(await this.clientConfig.client.execute(requestArg));
    }
    if (this.clientConfig.commandExecutor) {
      return normalizeServiceResult(await this.clientConfig.commandExecutor.execute(requestArg));
    }

    const command = this.commandForService(requestArg);
    if (!command) {
      return { success: false, error: `Unsupported IntelliFire local service or target: ${requestArg.domain}.${requestArg.service}` };
    }
    if (!this.hasEndpoint()) {
      return { success: false, error: 'IntelliFire local control requires config.host or config.url, or an injected client.execute/commandExecutor.' };
    }
    if (!this.clientConfig.userId || !this.clientConfig.apiKey) {
      return { success: false, error: 'IntelliFire local control requires config.userId and config.apiKey obtained from the vendor cloud/app flow.' };
    }

    try {
      await this.sendLocalCommand(command.command, command.value);
      this.cachedSnapshot = undefined;
      return { success: true, data: command };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await this.clientConfig.client?.destroy?.();
  }

  private async fetchPollData(): Promise<IIntellifirePollData> {
    return this.requestJson<IIntellifirePollData>('/poll');
  }

  private async sendLocalCommand(commandArg: TIntellifireLocalCommand, valueArg: number): Promise<void> {
    this.checkCommandRange(commandArg, valueArg);
    const challenge = (await this.requestText('/get_challenge')).trim();
    if (!isHex(challenge)) {
      throw new Error('IntelliFire local challenge response was not hexadecimal.');
    }

    const body = this.constructPayload(commandArg, valueArg, challenge);
    await this.requestText('/post', {
      method: 'POST',
      headers: { 'content-type': 'application/x-www-form-urlencoded' },
      body,
    });
  }

  private constructPayload(commandArg: TIntellifireLocalCommand, valueArg: number, challengeArg: string): string {
    const apiKey = this.clientConfig.apiKey || '';
    if (!isHex(apiKey)) {
      throw new Error('IntelliFire config.apiKey must be a hexadecimal local API key.');
    }

    const payload = `post:command=${commandArg}&value=${valueArg}`;
    const apiBytes = Buffer.from(apiKey, 'hex');
    const challengeBytes = Buffer.from(challengeArg, 'hex');
    const payloadBytes = Buffer.from(payload, 'utf8');
    const inner = plugins.crypto.createHash('sha256').update(Buffer.concat([apiBytes, challengeBytes, payloadBytes])).digest();
    const response = plugins.crypto.createHash('sha256').update(Buffer.concat([apiBytes, inner])).digest('hex');
    return new URLSearchParams({ command: commandArg, value: String(valueArg), user: this.clientConfig.userId || '', response }).toString();
  }

  private async requestJson<TValue>(pathArg: string): Promise<TValue> {
    const text = await this.requestText(pathArg, { headers: { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' } });
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new Error(`IntelliFire local JSON parse failed for ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestText(pathArg: string, initArg: RequestInit = {}): Promise<string> {
    const url = this.url(pathArg);
    const timeoutMs = this.clientConfig.timeoutMs || intellifireDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(url, { ...initArg, signal: controller.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`IntelliFire local request ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return text;
    } finally {
      clearTimeout(timer);
    }
  }

  private commandForService(requestArg: IServiceCallRequest): IIntellifireCommandSpec | undefined {
    const data = objectValue(requestArg.data) || {};
    const directCommand = stringValue(data.command);
    const directValue = numberValue(data.value);
    if (directCommand && directValue !== undefined && directCommand in commandRanges) {
      return { command: directCommand as TIntellifireLocalCommand, value: directValue };
    }

    const service = requestArg.service;
    const target = JSON.stringify(requestArg.target || {}).toLowerCase();
    const onValue = service === 'turn_on' || service === 'set_hvac_mode' && stringValue(data.hvac_mode) === 'heat' ? 1 : 0;

    if (requestArg.domain === 'switch') {
      return target.includes('pilot') ? { command: 'pilot', value: onValue } : { command: 'power', value: onValue };
    }
    if (requestArg.domain === 'light') {
      if (service === 'turn_off') {
        return { command: 'light', value: 0 };
      }
      const brightness = numberValue(data.brightness);
      return { command: 'light', value: clamp(brightness === undefined ? 2 : Math.round(brightness / 85), 0, 3) };
    }
    if (requestArg.domain === 'fan') {
      if (service === 'turn_off') {
        return { command: 'fan_speed', value: 0 };
      }
      const percentage = numberValue(data.percentage);
      return { command: 'fan_speed', value: clamp(percentage === undefined ? 1 : Math.ceil((percentage / 100) * 4), 0, 4) };
    }
    if (requestArg.domain === 'number' && service === 'set_value') {
      return { command: 'flame_height', value: clamp(Math.round((directValue ?? numberValue(data.native_value) ?? 1) - 1), 0, 4) };
    }
    if (requestArg.domain === 'climate') {
      if (service === 'set_temperature') {
        const temperature = numberValue(data.temperature);
        return temperature === undefined ? undefined : { command: 'thermostat_setpoint', value: clamp(Math.round(temperature * 100), 0, 3700) };
      }
      if (service === 'set_hvac_mode') {
        return stringValue(data.hvac_mode) === 'off' ? { command: 'thermostat_setpoint', value: 0 } : { command: 'power', value: 1 };
      }
    }
    if (requestArg.domain === 'intellifire') {
      if (service === 'beep') {
        return { command: 'beep', value: 1 };
      }
      if (service === 'soft_reset') {
        return { command: 'soft_reset', value: 1 };
      }
      if (service === 'set_sleep_timer') {
        const minutes = numberValue(data.minutes);
        return minutes === undefined ? undefined : { command: 'time_remaining', value: clamp(Math.round(minutes * 60), 0, 10800) };
      }
      if (service === 'stop_sleep_timer') {
        return { command: 'time_remaining', value: 0 };
      }
    }
    return undefined;
  }

  private checkCommandRange(commandArg: TIntellifireLocalCommand, valueArg: number): void {
    const range = commandRanges[commandArg];
    if (!Number.isInteger(valueArg) || valueArg < range.min || valueArg > range.max) {
      throw new Error(`IntelliFire command ${commandArg} value must be an integer between ${range.min} and ${range.max}.`);
    }
  }

  private url(pathArg: string): string {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('IntelliFire local API requires config.host or config.url.');
    }
    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    return new URL(path, `${endpoint.origin}/`).toString();
  }

  private endpoint(): URL | undefined {
    const configuredUrl = stringValue(this.clientConfig.url);
    if (configuredUrl) {
      return normalizeBaseUrl(configuredUrl, this.clientConfig.port);
    }
    const host = stringValue(this.clientConfig.ipAddress) || stringValue(this.clientConfig.host);
    if (!host) {
      return undefined;
    }
    return normalizeBaseUrl(host, this.clientConfig.port || intellifireDefaultPort);
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private hasManualData(): boolean {
    return this.rawData() !== undefined || Boolean(this.clientConfig.entities?.length || this.clientConfig.snapshot);
  }

  private rawData(): TIntellifireRawData | undefined {
    return this.clientConfig.rawData as TIntellifireRawData | undefined ?? this.clientConfig.rawText;
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return IntellifireMapper.toSnapshot({ config: this.clientConfig, online: false, source: 'runtime', error: errorArg });
  }
}

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg) {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

const normalizeBaseUrl = (valueArg: string, portArg?: number): URL => {
  const withProtocol = valueArg.includes('://') ? valueArg : `http://${valueArg}`;
  const url = new URL(withProtocol);
  url.pathname = '';
  url.search = '';
  url.hash = '';
  if (portArg) {
    url.port = String(portArg);
  }
  return url;
};

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

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

const objectValue = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
};

const isHex = (valueArg: string): boolean => {
  return valueArg.length > 0 && valueArg.length % 2 === 0 && /^[0-9a-f]+$/i.test(valueArg);
};

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

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