import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IPlugwiseConfig, IPlugwiseSnapshot } from './plugwise.types.js';
import { plugwiseDefaultName, plugwiseDefaultPort, plugwiseDefaultUsername, plugwiseDomainObjectsPath, plugwiseProfile } from './plugwise.types.js';

export class PlugwiseClient extends SimpleLocalClient<IPlugwiseConfig> {
  private liveSnapshot?: IPlugwiseSnapshot;

  constructor(private readonly configArg: IPlugwiseConfig) {
    super(plugwiseProfile, configArg);
  }

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

    if (this.shouldUseNativeHttp(forceRefreshArg)) {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.liveSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    this.liveSnapshot = await super.getSnapshot(forceRefreshArg) as IPlugwiseSnapshot;
    return SimpleLocalMapper.clone(this.liveSnapshot);
  }

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

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.endpoint()) {
      return super.execute(requestArg);
    }
    return {
      success: false,
      error: `Plugwise native HTTP polling is read-only; ${requestArg.domain}.${requestArg.service} requires an injected client.execute() or commandExecutor because Home Assistant delegates Plugwise write payload semantics to the plugwise Python library.`,
    };
  }

  private shouldUseNativeHttp(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    if (!forceRefreshArg && (this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length)) {
      return false;
    }
    return true;
  }

  private async fetchSnapshot(): Promise<IPlugwiseSnapshot> {
    const endpoint = this.requireEndpoint();
    const password = stringValue(this.configArg.password);
    if (!password) {
      throw new Error('Plugwise Smile HTTP polling requires config.password, the 8-character gateway ID from the device sticker.');
    }
    const path = this.configArg.domainObjectsPath || this.configArg.path || plugwiseDomainObjectsPath;
    const xml = await this.requestText(`${endpoint.baseUrl}${path.startsWith('/') ? path : `/${path}`}`, password);
    return this.snapshotFromDomainObjects(endpoint, xml);
  }

  private async requestText(urlArg: string, passwordArg: string): Promise<string> {
    const timeoutMs = this.configArg.timeoutMs || 10000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const username = this.configArg.username || plugwiseDefaultUsername;
      const response = await globalThis.fetch(urlArg, {
        headers: {
          accept: 'application/xml, text/xml;q=0.9, */*;q=0.8',
          authorization: `Basic ${Buffer.from(`${username}:${passwordArg}`).toString('base64')}`,
        },
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`Plugwise Smile HTTP request failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      if (!text.trim()) {
        throw new Error('Plugwise Smile returned an empty XML response.');
      }
      return text;
    } finally {
      clearTimeout(timer);
    }
  }

  private snapshotFromDomainObjects(endpointArg: IPlugwiseEndpoint, xmlArg: string): IPlugwiseSnapshot {
    const appliances = parseAppliances(xmlArg);
    const gatewayAppliance = appliances.find((applianceArg) => applianceArg.type === 'gateway') || appliances[0];
    const gatewayBlock = firstBlock(xmlArg, 'gateway') || gatewayAppliance?.block || '';
    const gatewayId = this.configArg.uniqueId || this.configArg.deviceId || gatewayAppliance?.id || textFromBlock(gatewayBlock, 'hostname') || `${endpointArg.host}:${endpointArg.port}`;
    const name = this.configArg.name || gatewayAppliance?.name || textFromBlock(gatewayBlock, 'hostname') || plugwiseDefaultName;
    const firmware = textFromBlock(gatewayBlock, 'firmware_version') || textFromBlock(gatewayAppliance?.block || '', 'firmware_version');
    const hardware = textFromBlock(gatewayBlock, 'hardware_version') || textFromBlock(gatewayAppliance?.block || '', 'hardware_version');
    const modelId = textFromBlock(gatewayBlock, 'vendor_model') || gatewayAppliance?.type;
    const macAddress = textFromBlock(gatewayBlock, 'mac_address') || this.configArg.macAddress;
    const measurementEntities = entitiesFromMeasurements(appliances);
    const entities: ISimpleLocalEntitySnapshot[] = measurementEntities.length ? measurementEntities : [{
      id: 'online',
      uniqueId: `${plugwiseProfile.domain}_${SimpleLocalMapper.slug(gatewayId)}_online`,
      name: 'Online',
      platform: 'binary_sensor',
      state: true,
      available: true,
      writable: false,
      attributes: { source: 'http' },
    }];

    return {
      device: {
        id: gatewayId,
        name,
        manufacturer: 'Plugwise',
        model: modelId || 'Plugwise Smile',
        serialNumber: macAddress,
        host: endpointArg.host,
        port: endpointArg.port,
        protocol: endpointArg.useTls ? 'https' : 'http',
        configurationUrl: endpointArg.baseUrl,
        attributes: cleanAttributes({
          firmware,
          hardware,
          hostname: textFromBlock(gatewayBlock, 'hostname'),
          macAddress,
          applianceCount: appliances.length,
        }),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: {
        xml: xmlArg,
        gateway: cleanAttributes({ firmware, hardware, modelId, macAddress }),
        appliances: appliances.map((applianceArg) => ({ id: applianceArg.id, name: applianceArg.name, type: applianceArg.type })),
      },
      capabilities: {
        localRead: true,
        localControl: false,
        plugwiseSmileHttp: true,
      },
    };
  }

  private requireEndpoint(): IPlugwiseEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Plugwise Smile HTTP polling requires config.host or config.url.');
    }
    return endpoint;
  }

  private endpoint(): IPlugwiseEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    if (rawUrl) {
      const parsed = parseUrl(rawUrl);
      if (parsed) {
        return endpointFromParts(parsed.hostname, this.configArg.port || numberValue(parsed.port) || plugwiseDefaultPort, parsed.protocol === 'https:' || this.configArg.useTls === true);
      }
    }
    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const parsedHost = parseUrl(host);
    if (parsedHost) {
      return endpointFromParts(parsedHost.hostname, this.configArg.port || numberValue(parsedHost.port) || plugwiseDefaultPort, parsedHost.protocol === 'https:' || this.configArg.useTls === true);
    }
    const hostPort = splitHostPort(host);
    return endpointFromParts(hostPort.host, this.configArg.port || hostPort.port || plugwiseDefaultPort, this.configArg.useTls === true);
  }

  private createOfflineSnapshot(errorArg: string): IPlugwiseSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: plugwiseProfile, config: this.configArg, online: false, source: 'runtime', error: errorArg }) as IPlugwiseSnapshot;
  }
}

interface IPlugwiseEndpoint {
  host: string;
  port: number;
  useTls: boolean;
  baseUrl: string;
}

interface IPlugwiseAppliance {
  id: string;
  name: string;
  type: string;
  block: string;
}

interface IXmlBlock {
  attrs: string;
  block: string;
}

const endpointFromParts = (hostArg: string, portArg: number, useTlsArg: boolean): IPlugwiseEndpoint => {
  const defaultPort = useTlsArg ? 443 : plugwiseDefaultPort;
  const portPart = portArg === defaultPort ? '' : `:${portArg}`;
  return { host: hostArg, port: portArg, useTls: useTlsArg, baseUrl: `${useTlsArg ? 'https' : 'http'}://${hostArg}${portPart}` };
};

const parseAppliances = (xmlArg: string): IPlugwiseAppliance[] => allBlocks(xmlArg, 'appliance').map((blockArg, indexArg) => ({
  id: attrValue(blockArg.attrs, 'id') || `appliance_${indexArg + 1}`,
  name: textFromBlock(blockArg.block, 'name') || `Appliance ${indexArg + 1}`,
  type: textFromBlock(blockArg.block, 'type') || attrValue(blockArg.attrs, 'type') || 'appliance',
  block: blockArg.block,
}));

const entitiesFromMeasurements = (appliancesArg: IPlugwiseAppliance[]): ISimpleLocalEntitySnapshot[] => {
  const entities: ISimpleLocalEntitySnapshot[] = [];
  for (const appliance of appliancesArg) {
    const measurements = allBlocks(appliance.block, 'measurement');
    for (let index = 0; index < measurements.length; index++) {
      const measurement = measurements[index];
      const type = textFromBlock(measurement.block, 'type') || attrValue(measurement.attrs, 'type') || attrValue(measurement.attrs, 'id') || `measurement_${index + 1}`;
      const value = textFromBlock(measurement.block, 'value') || textFromBlock(measurement.block, 'state');
      if (value === undefined) {
        continue;
      }
      const state = scalarValue(value);
      const unit = textFromBlock(measurement.block, 'unit') || textFromBlock(measurement.block, 'unit_of_measurement');
      const id = `${SimpleLocalMapper.slug(appliance.id)}_${SimpleLocalMapper.slug(type)}`;
      entities.push({
        id,
        uniqueId: `${plugwiseProfile.domain}_${id}`,
        name: `${appliance.name} ${titleCase(type)}`,
        platform: typeof state === 'boolean' && !unit ? 'binary_sensor' : 'sensor',
        state,
        unit,
        available: true,
        writable: false,
        attributes: cleanAttributes({
          applianceId: appliance.id,
          applianceType: appliance.type,
          measurementType: type,
          measurementId: attrValue(measurement.attrs, 'id'),
          source: 'http',
        }),
      });
    }
  }
  return entities;
};

const allBlocks = (xmlArg: string, tagArg: string): IXmlBlock[] => {
  const blocks: IXmlBlock[] = [];
  const pattern = new RegExp(`<${tagArg}\\b([^>]*)>[\\s\\S]*?</${tagArg}>`, 'gi');
  let match = pattern.exec(xmlArg);
  while (match) {
    blocks.push({ attrs: match[1] || '', block: match[0] });
    match = pattern.exec(xmlArg);
  }
  return blocks;
};

const firstBlock = (xmlArg: string, tagArg: string): string | undefined => allBlocks(xmlArg, tagArg)[0]?.block;

const textFromBlock = (blockArg: string, tagArg: string): string | undefined => {
  const match = new RegExp(`<${tagArg}\\b[^>]*>([\\s\\S]*?)</${tagArg}>`, 'i').exec(blockArg);
  if (!match) {
    return undefined;
  }
  const text = match[1].replace(/<[^>]+>/g, '').trim();
  return text ? decodeXml(text) : undefined;
};

const attrValue = (attrsArg: string, nameArg: string): string | undefined => {
  const match = new RegExp(`${nameArg}=["']([^"']+)["']`, 'i').exec(attrsArg);
  return match ? decodeXml(match[1]) : undefined;
};

const scalarValue = (valueArg: string): string | number | boolean => {
  const normalized = valueArg.trim();
  if (/^(true|on)$/i.test(normalized)) {
    return true;
  }
  if (/^(false|off)$/i.test(normalized)) {
    return false;
  }
  if (normalized && Number.isFinite(Number(normalized))) {
    return Number(normalized);
  }
  return normalized;
};

const decodeXml = (valueArg: string): string => valueArg
  .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
  .replace(/&lt;/g, '<')
  .replace(/&gt;/g, '>')
  .replace(/&quot;/g, '"')
  .replace(/&apos;/g, "'")
  .replace(/&amp;/g, '&');

const titleCase = (valueArg: string): string => valueArg.replace(/[_-]+/g, ' ').replace(/\b\w/g, (charArg) => charArg.toUpperCase());

const stringValue = (valueArg: unknown): string | undefined => 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() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};

const cleanAttributes = (attributesArg: Record<string, unknown>): Record<string, unknown> | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(clean).length ? clean : undefined;
};

const parseUrl = (valueArg: string): URL | undefined => {
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};

const splitHostPort = (valueArg: string): { host: string; port?: number } => {
  const match = /^([^:]+):(\d+)$/.exec(valueArg);
  return match ? { host: match[1], port: Number(match[2]) } : { host: valueArg };
};

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