import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import { KonnectedMapper } from './konnected.mapper.js';
import type { IKonnectedConfig, IKonnectedStatus, TKonnectedAddressMode } from './konnected.types.js';
import { konnectedProfile } from './konnected.types.js';

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

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

interface IKonnectedTarget {
  mode: TKonnectedAddressMode;
  value: string;
}

export class KonnectedClient extends SimpleLocalClient<IKonnectedConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;
  private latestStatus?: IKonnectedStatus;

  constructor(private readonly configArg: IKonnectedConfig) {
    super(konnectedProfile, 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.hasLiveHttpConfig()) {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
        return clone(this.liveSnapshot);
      } catch (errorArg) {
        this.liveSnapshot = KonnectedMapper.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.hasLiveHttpConfig()) {
      return {
        success: false,
        error: 'Konnected control requires config.host/config.url, an injected client.execute(), or commandExecutor.',
      };
    }

    try {
      if (!['turn_on', 'turn_off', 'toggle'].includes(requestArg.service)) {
        return { success: false, error: `Unsupported Konnected service: ${requestArg.service}` };
      }

      const target = await this.targetForRequest(requestArg);
      const state = requestArg.service === 'toggle'
        ? await this.toggleState(target, requestArg)
        : this.stateForService(requestArg.service, requestArg);
      const body = cleanBody({
        [target.mode]: numericOrString(target.value),
        state,
        momentary: requestArg.data?.momentary,
        times: requestArg.data?.times ?? requestArg.data?.repeat,
        pause: requestArg.data?.pause,
      });
      const response = await this.requestJson<unknown>(this.pathForMode(target.mode), { method: 'PUT', body });

      this.liveSnapshot = undefined;
      return { success: true, data: { target, state, response } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const status = await this.requestJson<IKonnectedStatus>('/status');
    this.latestStatus = status;

    const endpoint = this.endpoint();
    const host = endpoint?.host || this.configArg.host;
    const port = endpoint?.port || this.configArg.port || konnectedProfile.defaultPort;
    const deviceId = this.configArg.uniqueId || this.configArg.deviceId || status.chipId || stripMac(status.mac) || (host ? `${host}:${port || ''}` : undefined);
    const model = status.model || this.configArg.model || konnectedProfile.model;
    const entities = [
      ...arrayValue(status.sensors).map((sensorArg) => {
        const target = targetFromRecord(sensorArg, this.addressMode(status));
        return {
          id: `${target.mode}_${target.value}`,
          uniqueId: `${deviceId || host || 'konnected'}_${target.mode}_${target.value}`,
          name: `${titleCase(target.mode)} ${target.value}`,
          platform: 'binary_sensor' as const,
          state: boolState(sensorArg.state),
          available: true,
          writable: false,
          attributes: cleanBody({ ...sensorArg, addressMode: target.mode, address: target.value }),
        };
      }),
      ...arrayValue(status.actuators).map((actuatorArg) => {
        const target = targetFromRecord(actuatorArg, this.addressMode(status));
        return {
          id: `actuator_${target.mode}_${target.value}`,
          uniqueId: `${deviceId || host || 'konnected'}_actuator_${target.mode}_${target.value}`,
          name: `Actuator ${titleCase(target.mode)} ${target.value}`,
          platform: 'switch' as const,
          state: boolState(actuatorArg.state) ?? null,
          available: true,
          writable: true,
          attributes: cleanBody({ ...actuatorArg, addressMode: target.mode, address: target.value }),
        };
      }),
      ...statusMetrics(status),
    ];

    const rawData = {
      device: {
        id: deviceId,
        name: this.configArg.name || model || konnectedProfile.defaultName,
        manufacturer: 'Konnected.io',
        model,
        serialNumber: status.chipId || stripMac(status.mac),
        host,
        port,
        protocol: endpoint?.protocol || 'http',
        configurationUrl: host ? `${endpoint?.protocol || 'http'}://${host}${port ? `:${port}` : ''}` : undefined,
        attributes: cleanBody({ mac: status.mac, swVersion: status.swVersion, hwVersion: status.hwVersion }),
      },
      status,
      entities,
      fetchedAt: new Date().toISOString(),
    };

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

  private async targetForRequest(requestArg: IServiceCallRequest): Promise<IKonnectedTarget> {
    const data = requestArg.data || {};
    if (data.zone !== undefined) {
      return { mode: 'zone', value: String(data.zone) };
    }
    if (data.pin !== undefined) {
      return { mode: 'pin', value: String(data.pin) };
    }

    const parsed = targetFromText(requestArg.target.entityId) || targetFromText(requestArg.target.deviceId);
    if (parsed) {
      return parsed;
    }

    const snapshot = await this.getSnapshot();
    const switchEntities = snapshot.entities.filter((entityArg) => entityArg.platform === 'switch' && entityArg.attributes?.addressMode && entityArg.attributes?.address);
    if (switchEntities.length === 1) {
      return { mode: switchEntities[0].attributes!.addressMode as TKonnectedAddressMode, value: String(switchEntities[0].attributes!.address) };
    }

    throw new KonnectedClientError('Konnected control requires data.zone/data.pin or a target entity id containing a zone/pin address.');
  }

  private async toggleState(targetArg: IKonnectedTarget, requestArg: IServiceCallRequest): Promise<number> {
    const response = await this.requestJson<unknown>(this.pathForMode(targetArg.mode), { query: { [targetArg.mode]: targetArg.value } });
    const first = Array.isArray(response) ? response[0] : response;
    const current = first && typeof first === 'object' ? boolState((first as Record<string, unknown>).state) : undefined;
    if (current === undefined) {
      throw new KonnectedClientError('Konnected toggle requires the panel to return the current state from /device or /zone.');
    }
    return current ? this.stateForService('turn_off', requestArg) : this.stateForService('turn_on', requestArg);
  }

  private stateForService(serviceArg: string, requestArg: IServiceCallRequest): number {
    if (requestArg.data?.state !== undefined) {
      return boolState(requestArg.data.state) ? 1 : 0;
    }
    const activation = String(requestArg.data?.activation || this.configArg.activation || 'high').toLowerCase();
    if (serviceArg === 'turn_on') {
      return activation === 'low' ? 0 : 1;
    }
    return activation === 'low' ? 1 : 0;
  }

  private pathForMode(modeArg: TKonnectedAddressMode): string {
    return modeArg === 'zone' ? '/zone' : '/device';
  }

  private addressMode(statusArg?: IKonnectedStatus): TKonnectedAddressMode {
    if (this.configArg.apiVersion === 'zone' || this.configArg.addressMode === 'zone') {
      return 'zone';
    }
    if (this.configArg.apiVersion === 'pin' || this.configArg.addressMode === 'pin') {
      return 'pin';
    }
    return (statusArg?.model || this.configArg.model) === 'Konnected Pro' ? 'zone' : 'pin';
  }

  private async requestJson<TValue>(pathArg: string, optionsArg: { method?: 'GET' | 'PUT'; body?: Record<string, unknown>; query?: Record<string, unknown> } = {}): Promise<TValue> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new KonnectedClientError('Konnected HTTP requests require config.host or config.url.');
    }

    const url = this.urlFor(endpoint, pathArg, optionsArg.query);
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.configArg.timeoutMs || 5000);

    try {
      const response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers: cleanBody({
          accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
          'content-type': optionsArg.body ? 'application/json' : undefined,
        }) as Record<string, string>,
        body: optionsArg.body ? JSON.stringify(optionsArg.body) : undefined,
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new KonnectedClientError(`Konnected HTTP ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return parseJson(text) as TValue;
    } finally {
      clearTimeout(timer);
    }
  }

  private urlFor(endpointArg: IKonnectedEndpoint, pathArg: string, queryArg?: Record<string, unknown>): string {
    const port = endpointArg.port && !((endpointArg.protocol === 'https' && endpointArg.port === 443) || (endpointArg.protocol === 'http' && endpointArg.port === 80)) ? `:${endpointArg.port}` : '';
    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    const url = new URL(`${endpointArg.protocol}://${endpointArg.host}${port}${path}`);
    for (const [key, value] of Object.entries(queryArg || {})) {
      if (value !== undefined && value !== null) {
        url.searchParams.set(key, String(value));
      }
    }
    return url.toString();
  }

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

  private endpoint(): IKonnectedEndpoint | 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,
        };
      } catch {
        return undefined;
      }
    }
    if (!this.configArg.host) {
      return undefined;
    }
    return {
      protocol: this.configArg.useTls ? 'https' : 'http',
      host: this.configArg.host,
      port: this.configArg.port || konnectedProfile.defaultPort,
    };
  }
}

const arrayValue = (valueArg: unknown): Array<Record<string, unknown>> => Array.isArray(valueArg) ? valueArg.filter((itemArg): itemArg is Record<string, unknown> => Boolean(itemArg && typeof itemArg === 'object' && !Array.isArray(itemArg))) : [];

const targetFromRecord = (recordArg: Record<string, unknown>, fallbackModeArg: TKonnectedAddressMode): IKonnectedTarget => {
  if (recordArg.zone !== undefined) {
    return { mode: 'zone', value: String(recordArg.zone) };
  }
  if (recordArg.pin !== undefined) {
    return { mode: 'pin', value: String(recordArg.pin) };
  }
  return { mode: fallbackModeArg, value: String(recordArg[fallbackModeArg] || 'unknown') };
};

const targetFromText = (valueArg: unknown): IKonnectedTarget | undefined => {
  if (typeof valueArg !== 'string') {
    return undefined;
  }
  const explicit = valueArg.match(/(zone|pin)[._-]?([a-z0-9]+)/i);
  if (explicit) {
    return { mode: explicit[1].toLowerCase() as TKonnectedAddressMode, value: explicit[2] };
  }
  return undefined;
};

const statusMetrics = (statusArg: IKonnectedStatus) => ['rssi', 'uptime', 'heap', 'port']
  .filter((keyArg) => statusArg[keyArg] !== undefined)
  .map((keyArg) => ({
    id: keyArg,
    name: titleCase(keyArg),
    platform: 'sensor' as const,
    state: statusArg[keyArg],
    available: true,
    writable: false,
    attributes: { source: 'status' },
  }));

const boolState = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'number') {
    return valueArg !== 0;
  }
  if (typeof valueArg === 'string') {
    const normalized = valueArg.trim().toLowerCase();
    if (['1', 'true', 'on', 'high'].includes(normalized)) {
      return true;
    }
    if (['0', 'false', 'off', 'low'].includes(normalized)) {
      return false;
    }
  }
  return undefined;
};

const numericOrString = (valueArg: string): string | number => Number.isFinite(Number(valueArg)) ? Number(valueArg) : valueArg;
const stripMac = (valueArg: unknown): string | undefined => typeof valueArg === 'string' ? valueArg.replace(/[^a-f0-9]/gi, '').toLowerCase() : undefined;
const titleCase = (valueArg: string): string => valueArg.replace(/[_-]+/g, ' ').replace(/\b\w/g, (charArg) => charArg.toUpperCase());
const parseJson = (textArg: string): unknown => textArg.trim() ? JSON.parse(textArg) : 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);
const cleanBody = <TValue extends Record<string, unknown>>(valueArg: TValue): Partial<TValue> => Object.fromEntries(Object.entries(valueArg).filter(([, value]) => value !== undefined)) as Partial<TValue>;
