import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import { KmtronicMapper } from './kmtronic.mapper.js';
import type { IKmtronicConfig, IKmtronicRelayState } from './kmtronic.types.js';
import { kmtronicProfile } from './kmtronic.types.js';

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

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

export class KmtronicClient extends SimpleLocalClient<IKmtronicConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IKmtronicConfig) {
    super(kmtronicProfile, 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 = KmtronicMapper.toSnapshot({
          config: this.configArg,
          online: false,
          source: 'runtime',
          error: errorMessage(errorArg),
        });
        return clone(this.liveSnapshot);
      }
    }

    if (this.endpoint() && !this.configArg.username && !this.configArg.client && !this.configArg.snapshotProvider && !this.configArg.snapshot) {
      if (this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length) {
        return KmtronicMapper.toSnapshot({
          config: this.configArg,
          rawData: this.configArg.rawData ?? this.configArg.rawText,
          online: this.configArg.online ?? true,
          source: 'manual',
        });
      }
      this.liveSnapshot = KmtronicMapper.toSnapshot({
        config: this.configArg,
        online: false,
        source: 'runtime',
        error: 'KMtronic live HTTP polling requires config.username and config.password for Basic authentication.',
      });
      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: 'KMtronic relay control requires config.host/config.url plus username/password, an injected client.execute(), or commandExecutor.',
      };
    }

    try {
      const relayId = await this.relayIdForRequest(requestArg);
      let path: string;
      let targetState: boolean | undefined;

      switch (requestArg.service) {
        case 'turn_on':
          targetState = !this.configArg.reverse;
          path = `FF${relayId.toString().padStart(2, '0')}${targetState ? '01' : '00'}`;
          break;
        case 'turn_off':
          targetState = Boolean(this.configArg.reverse);
          path = `FF${relayId.toString().padStart(2, '0')}${targetState ? '01' : '00'}`;
          break;
        case 'toggle':
          path = `relays.cgi?relay=${relayId}`;
          break;
        default:
          return { success: false, error: `Unsupported KMtronic service: ${requestArg.service}` };
      }

      const responseText = await this.requestText(path);
      this.liveSnapshot = undefined;
      return {
        success: true,
        data: {
          relayId,
          path,
          state: targetState,
          response: responseText,
        },
      };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const statusXml = await this.requestText('status.xml');
    const relays = parseRelayStatus(statusXml);
    if (!relays.length) {
      throw new KmtronicClientError('KMtronic status.xml did not contain relay states.');
    }

    const endpoint = this.endpoint();
    const host = endpoint?.host || this.configArg.host;
    const port = endpoint?.port || this.configArg.port || kmtronicProfile.defaultPort;
    const rawData = {
      device: {
        id: this.configArg.uniqueId || this.configArg.deviceId || (host ? `${host}:${port || ''}` : undefined),
        name: this.configArg.name || (host ? `KMtronic ${host}` : kmtronicProfile.defaultName),
        manufacturer: kmtronicProfile.manufacturer,
        model: kmtronicProfile.model,
        host,
        port,
        protocol: endpoint?.protocol || 'http',
        configurationUrl: host ? `${endpoint?.protocol || 'http'}://${host}${port && port !== 80 ? `:${port}` : ''}` : undefined,
      },
      relays,
      entities: relays.map((relayArg) => ({
        id: `relay_${relayArg.id}`,
        uniqueId: `${this.configArg.uniqueId || host || 'kmtronic'}_relay_${relayArg.id}`,
        name: `Relay ${relayArg.id}`,
        platform: 'switch' as const,
        state: this.configArg.reverse ? !relayArg.isEnergised : relayArg.isEnergised,
        available: true,
        writable: true,
        attributes: {
          relayId: relayArg.id,
          isEnergised: relayArg.isEnergised,
          reverse: this.configArg.reverse || undefined,
        },
      })),
      fetchedAt: new Date().toISOString(),
    };

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

  private async relayIdForRequest(requestArg: IServiceCallRequest): Promise<number> {
    const direct = numberValue(requestArg.data?.relayId) ?? numberValue(requestArg.data?.relay) ?? numberValue(requestArg.data?.channel) ?? numberValue(requestArg.data?.id);
    if (direct && direct > 0) {
      return direct;
    }

    const fromTarget = relayIdFromText(requestArg.target.entityId) ?? relayIdFromText(requestArg.target.deviceId);
    if (fromTarget) {
      return fromTarget;
    }

    const snapshot = await this.getSnapshot();
    const relayEntities = snapshot.entities.filter((entityArg) => entityArg.attributes?.relayId !== undefined);
    if (relayEntities.length === 1) {
      return Number(relayEntities[0].attributes!.relayId);
    }

    throw new KmtronicClientError('KMtronic control requires data.relayId/data.relay or a target entity id containing the relay number.');
  }

  private async requestText(pathArg: string): Promise<string> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new KmtronicClientError('KMtronic HTTP requests require config.host or config.url.');
    }
    if (!this.configArg.username) {
      throw new KmtronicClientError('KMtronic HTTP requests require config.username and config.password for Basic authentication.');
    }

    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    const port = endpoint.port && !((endpoint.protocol === 'https' && endpoint.port === 443) || (endpoint.protocol === 'http' && endpoint.port === 80)) ? `:${endpoint.port}` : '';
    const url = `${endpoint.protocol}://${endpoint.host}${port}${path}`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeoutMs());

    try {
      const response = await globalThis.fetch(url, {
        method: 'GET',
        headers: {
          accept: 'application/xml, text/xml, text/plain;q=0.9, */*;q=0.8',
          authorization: `Basic ${Buffer.from(`${this.configArg.username}:${this.configArg.password || ''}`).toString('base64')}`,
        },
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new KmtronicClientError(`KMtronic HTTP ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return text;
    } finally {
      clearTimeout(timer);
    }
  }

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

  private endpoint(): IKmtronicEndpoint | 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 || kmtronicProfile.defaultPort,
    };
  }

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

const parseRelayStatus = (xmlArg: string): IKmtronicRelayState[] => {
  const relays: IKmtronicRelayState[] = [];
  const relayRegex = /<relay(\d+)\b[^>]*>\s*([01])\s*<\/relay\1>/gi;
  for (let match = relayRegex.exec(xmlArg); match; match = relayRegex.exec(xmlArg)) {
    relays.push({ id: Number(match[1]), isEnergised: match[2] === '1' });
  }
  return relays.sort((leftArg, rightArg) => leftArg.id - rightArg.id);
};

const relayIdFromText = (valueArg: unknown): number | undefined => {
  if (typeof valueArg !== 'string') {
    return undefined;
  }
  const match = valueArg.match(/(?:relay|switch|channel)[._-]?(\d+)/i) || valueArg.match(/(\d+)$/);
  return match ? Number(match[1]) : undefined;
};

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return Math.round(valueArg);
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Math.round(Number(valueArg));
  }
  return 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);
