import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { IIsy994Config, TIsy994RawData } from './isy994.types.js';
import { isy994Profile } from './isy994.types.js';

export class Isy994Client extends SimpleLocalClient<IIsy994Config> {
  private snapshotCache?: ISimpleLocalSnapshot;

  constructor(configArg: IIsy994Config) {
    super(isy994Profile, configArg);
    this.localConfig = configArg;
  }

  private readonly localConfig: IIsy994Config;

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

    try {
      this.snapshotCache = await this.fetchSnapshot();
    } catch (errorArg) {
      this.snapshotCache = this.localOfflineSnapshot(errorMessage(errorArg));
    }
    return SimpleLocalMapper.clone(this.snapshotCache);
  }

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

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.canUseIsyRest()) {
      return super.execute(requestArg);
    }

    try {
      const response = await this.executeIsyRestCommand(requestArg.service, requestArg.data || {}, requestArg.target?.entityId || requestArg.target?.deviceId);
      this.snapshotCache = undefined;
      return { success: true, data: response };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const [configXml, nodesXml, statusXml] = await Promise.all([
      this.requestText(['config']),
      this.requestText(['nodes'], { members: 'false' }),
      this.requestText(['status']),
    ]);
    const config = parseConfigXml(configXml);
    const nodes = mergeNodeStatuses(parseNodesXml(nodesXml), parseStatusXml(statusXml));
    const rawData: TIsy994RawData = this.toRawData(config, nodes, { configXml, nodesXml, statusXml });
    return SimpleLocalMapper.toSnapshot({ profile: isy994Profile, config: this.localConfig, rawData, online: true, source: 'http' });
  }

  private async executeIsyRestCommand(serviceArg: string, dataArg: Record<string, unknown>, targetArg?: string): Promise<Record<string, unknown>> {
    const service = serviceArg.replace(/^isy994\./, '');
    if (service === 'snapshot' || service === 'status') {
      return { snapshot: await this.getSnapshot(true) };
    }
    if (service === 'refresh' || service === 'reload') {
      return { refresh: await this.refresh() };
    }
    if (service === 'send_program_command') {
      const address = this.commandAddress(dataArg, targetArg);
      const command = isyProgramCommand(stringValue(dataArg.command) || stringValue(dataArg.action));
      if (!address || !command) {
        throw new Error('ISY/IoX send_program_command requires data.address and data.command.');
      }
      const text = await this.requestText(['programs', address, command]);
      return { transmitted: true, path: ['programs', address, command].join('/'), response: text };
    }

    const address = this.commandAddress(dataArg, targetArg);
    if (!address) {
      throw new Error(`ISY/IoX service ${serviceArg} requires data.address because Home Assistant entity IDs cannot be reverse-mapped without the HA registry.`);
    }

    if (service === 'enable' || service === 'disable') {
      const text = await this.requestText(['nodes', address, service]);
      return { transmitted: true, path: ['nodes', address, service].join('/'), response: text };
    }

    const command = this.nodeCommand(service, dataArg);
    const value = commandValue(service, dataArg);
    const uom = stringValue(dataArg.unit_of_measurement) || stringValue(dataArg.unitOfMeasurement) || stringValue(dataArg.uom);
    const query = queryParameters(dataArg.parameters);
    const path = ['nodes', address, 'cmd', command, value, uom].filter((valueArg): valueArg is string => Boolean(valueArg));
    const text = await this.requestText(path, query);
    return { transmitted: true, path: path.join('/'), response: text };
  }

  private commandAddress(dataArg: Record<string, unknown>, targetArg?: string): string | undefined {
    return stringValue(dataArg.address) || stringValue(dataArg.nodeAddress) || stringValue(dataArg.programAddress) || stringValue(dataArg.id) || addressFromTarget(targetArg);
  }

  private nodeCommand(serviceArg: string, dataArg: Record<string, unknown>): string {
    if (serviceArg === 'send_raw_node_command' || serviceArg === 'set_value' || serviceArg === 'select_option') {
      const rawCommand = stringValue(dataArg.command) || stringValue(dataArg.control);
      if (!rawCommand) {
        throw new Error(`ISY/IoX ${serviceArg} requires data.command.`);
      }
      return rawCommand.toUpperCase();
    }
    if (serviceArg === 'send_node_command') {
      const command = isyNodeCommand(stringValue(dataArg.command) || stringValue(dataArg.action));
      if (!command) {
        throw new Error('ISY/IoX send_node_command requires a supported data.command.');
      }
      return command;
    }
    if (serviceArg === 'set_hvac_mode') {
      return 'CLIMD';
    }
    if (serviceArg === 'set_temperature') {
      return stringValue(dataArg.command)?.toUpperCase() || 'CLISPC';
    }

    const mapped = isyNodeCommand(serviceArg);
    if (!mapped) {
      throw new Error(`ISY/IoX service ${serviceArg} is not implemented by the REST client.`);
    }
    return mapped;
  }

  private toRawData(configArg: IIsyConfigXml, nodesArg: IIsyNodeRecord[], xmlArg: Record<string, string>): TIsy994RawData {
    const deviceId = this.localConfig.uniqueId || configArg.uuid || endpointId(this.localConfig);
    const entities: ISimpleLocalEntitySnapshot[] = nodesArg.map((nodeArg) => ({
      id: SimpleLocalMapper.slug(nodeArg.address),
      uniqueId: `${deviceId}_${SimpleLocalMapper.slug(nodeArg.address)}`,
      name: nodeArg.name || nodeArg.address,
      platform: platformForNode(nodeArg),
      state: stateForNode(nodeArg),
      available: nodeArg.enabled !== false,
      writable: nodeArg.kind !== 'folder' && platformForNode(nodeArg) !== 'sensor' && platformForNode(nodeArg) !== 'binary_sensor',
      unit: unitForUom(nodeArg.uom),
      attributes: cleanAttributes({
        address: nodeArg.address,
        kind: nodeArg.kind,
        family: nodeArg.family,
        type: nodeArg.type,
        nodeDefId: nodeArg.nodeDefId,
        parent: nodeArg.parent,
        primaryNode: nodeArg.primaryNode,
        status: nodeArg.status,
        formatted: nodeArg.formatted,
        uom: nodeArg.uom,
        precision: nodeArg.precision,
        members: nodeArg.members,
      }),
    }));

    return {
      device: {
        id: deviceId,
        name: this.localConfig.name || configArg.name || isy994Profile.defaultName || isy994Profile.displayName,
        manufacturer: 'Universal Devices, Inc',
        model: configArg.model || isy994Profile.model,
        serialNumber: configArg.uuid,
        host: endpointHost(this.localConfig),
        port: endpointPort(this.localConfig),
        protocol: endpointProtocol(this.localConfig),
        configurationUrl: this.baseUrl(),
        attributes: cleanAttributes({ firmware: configArg.firmware, variables: configArg.variables, nodedefs: configArg.nodedefs }),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: { config: configArg, nodes: nodesArg, xml: xmlArg },
    };
  }

  private async requestText(pathSegmentsArg: string[], queryArg?: Record<string, string>): Promise<string> {
    const url = this.restUrl(pathSegmentsArg, queryArg);
    const timeoutMs = this.localConfig.timeoutMs || 10000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const headers: Record<string, string> = { accept: 'application/xml, text/xml, text/plain;q=0.9, */*;q=0.8' };
      if (this.localConfig.username) {
        headers.authorization = `Basic ${Buffer.from(`${this.localConfig.username}:${this.localConfig.password || ''}`).toString('base64')}`;
      }
      const response = await globalThis.fetch(url, { headers, signal: controller.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`ISY/IoX REST request ${pathSegmentsArg.join('/')} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return text;
    } finally {
      clearTimeout(timer);
    }
  }

  private restUrl(pathSegmentsArg: string[], queryArg?: Record<string, string>): string {
    const endpoint = endpointUrl(this.localConfig);
    const portPart = endpoint.port && !((endpoint.protocol === 'https' && endpoint.port === 443) || (endpoint.protocol === 'http' && endpoint.port === 80)) ? `:${endpoint.port}` : '';
    const webroot = endpoint.webroot ? `/${endpoint.webroot.replace(/^\/+|\/+$/g, '')}` : '';
    const path = `${webroot}/rest/${pathSegmentsArg.map((segmentArg) => encodeURIComponent(segmentArg)).join('/')}`;
    const query = queryArg && Object.keys(queryArg).length ? `?${new URLSearchParams(queryArg).toString()}` : '';
    return `${endpoint.protocol}://${endpoint.host}${portPart}${path}${query}`;
  }

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

  private canUseIsyRest(): boolean {
    return Boolean(endpointHost(this.localConfig));
  }

  private localOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: isy994Profile, config: this.localConfig, online: false, source: 'runtime', error: errorArg });
  }
}

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

interface IIsyConfigXml {
  uuid?: string;
  name?: string;
  model?: string;
  firmware?: string;
  variables?: boolean;
  nodedefs?: boolean;
}

interface IIsyNodeRecord {
  kind: 'node' | 'group' | 'folder';
  address: string;
  name?: string;
  parent?: string;
  primaryNode?: string;
  family?: string;
  type?: string;
  nodeDefId?: string;
  enabled?: boolean;
  status?: string | number | boolean;
  formatted?: string;
  uom?: string;
  precision?: string;
  members?: string[];
}

const endpointUrl = (configArg: IIsy994Config): IIsyEndpoint => {
  const rawUrl = stringValue(configArg.url) || (stringValue(configArg.host)?.includes('://') ? stringValue(configArg.host) : undefined);
  if (rawUrl) {
    const url = new URL(rawUrl);
    const webroot = isyWebroot(url.pathname, stringValue(configArg.path));
    return { protocol: url.protocol === 'https:' ? 'https' : 'http', host: url.hostname, port: url.port ? Number(url.port) : undefined, webroot };
  }
  return {
    protocol: configArg.useTls ? 'https' : 'http',
    host: stringValue(configArg.host) || '',
    port: configArg.port || (configArg.useTls ? 443 : isy994Profile.defaultPort),
    webroot: isyWebroot('', stringValue(configArg.path)),
  };
};

const isyWebroot = (urlPathArg: string, configPathArg?: string): string | undefined => {
  const rawPath = (configPathArg || urlPathArg || '').replace(/\/desc$/i, '').replace(/\/$/, '');
  if (!rawPath || rawPath === '/' || rawPath.startsWith('/rest')) {
    return undefined;
  }
  return rawPath.replace(/^\/+|\/+$/g, '') || undefined;
};

const endpointHost = (configArg: IIsy994Config): string | undefined => endpointUrl(configArg).host || undefined;
const endpointPort = (configArg: IIsy994Config): number | undefined => endpointUrl(configArg).port;
const endpointProtocol = (configArg: IIsy994Config): 'http' | 'https' => endpointUrl(configArg).protocol;
const endpointId = (configArg: IIsy994Config): string => `${endpointHost(configArg) || 'isy994'}:${endpointPort(configArg) || isy994Profile.defaultPort || 80}`;

const parseConfigXml = (xmlArg: string): IIsyConfigXml => ({
  firmware: xmlText(xmlArg, 'app_full_version') || xmlText(xmlArg, 'firmware'),
  uuid: xmlText(xmlBlock(xmlArg, 'root') || xmlArg, 'id') || xmlText(xmlArg, 'uuid'),
  name: xmlText(xmlBlock(xmlArg, 'root') || xmlArg, 'name'),
  model: xmlText(xmlBlock(xmlArg, 'product') || xmlArg, 'desc') || xmlText(xmlArg, 'model'),
  variables: xmlBoolean(xmlText(xmlArg, 'variables')),
  nodedefs: xmlBoolean(xmlText(xmlArg, 'nodedefs')),
});

const parseNodesXml = (xmlArg: string): IIsyNodeRecord[] => {
  const records: IIsyNodeRecord[] = [];
  for (const nodeBlock of xmlBlocks(xmlArg, 'node')) {
    const openTag = nodeBlock.match(/^<node\b[^>]*>/i)?.[0] || '';
    const property = parseProperty(nodeBlock);
    const address = xmlText(nodeBlock, 'address') || attr(openTag, 'id');
    if (!address) {
      continue;
    }
    records.push({
      kind: 'node',
      address,
      name: xmlText(nodeBlock, 'name'),
      parent: xmlText(nodeBlock, 'parent'),
      primaryNode: xmlText(nodeBlock, 'pnode'),
      family: xmlText(nodeBlock, 'family'),
      type: xmlText(nodeBlock, 'type'),
      nodeDefId: attr(openTag, 'nodeDefId') || xmlText(nodeBlock, 'nodeDefId'),
      enabled: xmlBoolean(xmlText(nodeBlock, 'enabled')),
      ...property,
    });
  }
  for (const groupBlock of xmlBlocks(xmlArg, 'group')) {
    const address = xmlText(groupBlock, 'address');
    if (!address) {
      continue;
    }
    records.push({
      kind: 'group',
      address,
      name: xmlText(groupBlock, 'name'),
      parent: xmlText(groupBlock, 'parent'),
      primaryNode: xmlText(groupBlock, 'pnode'),
      family: xmlText(groupBlock, 'family'),
      members: xmlBlocks(groupBlock, 'link').map((linkArg) => stripXml(linkArg)).filter(Boolean),
      status: 0,
    });
  }
  return records;
};

const parseStatusXml = (xmlArg: string): Map<string, Partial<IIsyNodeRecord>> => {
  const statuses = new Map<string, Partial<IIsyNodeRecord>>();
  for (const nodeBlock of xmlBlocks(xmlArg, 'node')) {
    const openTag = nodeBlock.match(/^<node\b[^>]*>/i)?.[0] || '';
    const address = attr(openTag, 'id') || xmlText(nodeBlock, 'address');
    if (!address) {
      continue;
    }
    statuses.set(address, parseProperty(nodeBlock));
  }
  return statuses;
};

const mergeNodeStatuses = (nodesArg: IIsyNodeRecord[], statusesArg: Map<string, Partial<IIsyNodeRecord>>): IIsyNodeRecord[] => nodesArg.map((nodeArg) => ({ ...nodeArg, ...(statusesArg.get(nodeArg.address) || {}) }));

const parseProperty = (xmlArg: string): Partial<IIsyNodeRecord> => {
  const propertyTags = [...xmlArg.matchAll(/<property\b([^>]*)\/?>(?:[\s\S]*?<\/property>)?/gi)].map((matchArg) => matchArg[0]);
  const statusProperty = propertyTags.find((propertyArg) => attr(propertyArg, 'id') === 'ST') || propertyTags[0];
  if (!statusProperty) {
    return {};
  }
  const value = attr(statusProperty, 'value') || attr(statusProperty, 'val') || attr(statusProperty, 'status') || stripXml(statusProperty);
  const formatted = attr(statusProperty, 'formatted') || attr(statusProperty, 'fmtAct') || xmlText(statusProperty, 'formatted') || xmlText(statusProperty, 'fmtAct');
  return cleanAttributes({
    status: coerceStatus(value, attr(statusProperty, 'uom')),
    formatted,
    uom: attr(statusProperty, 'uom'),
    precision: attr(statusProperty, 'prec'),
  });
};

const platformForNode = (nodeArg: IIsyNodeRecord): ISimpleLocalEntitySnapshot['platform'] => {
  if (nodeArg.kind === 'group') {
    return 'switch';
  }
  const type = nodeArg.type || '';
  const nodeDefId = (nodeArg.nodeDefId || '').toLowerCase();
  if (nodeDefId.includes('binary') || nodeDefId.includes('alarm')) {
    return 'binary_sensor';
  }
  if (nodeDefId.includes('thermostat') || type.startsWith('5.')) {
    return 'climate';
  }
  if (type.startsWith('1.')) {
    return 'light';
  }
  if (type.startsWith('2.') || type.startsWith('7.')) {
    return 'switch';
  }
  if (type.startsWith('14.')) {
    return 'cover';
  }
  if (nodeArg.uom === '2' || nodeArg.uom === '78') {
    return 'binary_sensor';
  }
  return 'sensor';
};

const stateForNode = (nodeArg: IIsyNodeRecord): unknown => {
  if (nodeArg.status !== undefined) {
    return nodeArg.status;
  }
  if (nodeArg.formatted !== undefined) {
    return nodeArg.formatted;
  }
  return nodeArg.enabled !== false;
};

const unitForUom = (uomArg?: string): string | undefined => {
  const units: Record<string, string> = { '4': '°C', '17': '°F', '51': '%', '57': 's', '72': 'V', '73': 'W', '100': '%', '101': '° (x2)' };
  return uomArg ? units[uomArg] : undefined;
};

const isyNodeCommand = (valueArg?: string): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const normalized = valueArg.toLowerCase();
  const commands: Record<string, string> = {
    beep: 'BEEP',
    brighten: 'BRT',
    dim: 'DIM',
    fade_down: 'FDDOWN',
    fade_stop: 'FDSTOP',
    fade_up: 'FDUP',
    fast_off: 'DFOF',
    fast_on: 'DFON',
    open_cover: 'DON',
    close_cover: 'DOF',
    press: 'DON',
    stop_cover: 'FDSTOP',
    toggle: 'DON',
    turn_off: 'DOF',
    turn_on: 'DON',
  };
  return commands[normalized] || valueArg.toUpperCase();
};

const isyProgramCommand = (valueArg?: string): string | undefined => {
  if (!valueArg) {
    return undefined;
  }
  const commands: Record<string, string> = {
    disable: 'disable',
    disable_run_at_startup: 'disableRunAtStartup',
    enable: 'enable',
    enable_run_at_startup: 'enableRunAtStartup',
    run: 'run',
    run_else: 'runElse',
    run_then: 'runThen',
    stop: 'stop',
  };
  return commands[valueArg.toLowerCase()] || valueArg;
};

const commandValue = (serviceArg: string, dataArg: Record<string, unknown>): string | undefined => {
  if (serviceArg === 'set_hvac_mode') {
    const mode = stringValue(dataArg.hvacMode) || stringValue(dataArg.hvac_mode) || stringValue(dataArg.mode);
    const modes: Record<string, string> = { off: '0', heat: '1', cool: '2', heat_cool: '3', auto: '3', fan_only: '4' };
    return mode ? modes[mode] || mode : undefined;
  }
  const value = dataArg.value ?? dataArg.brightness ?? dataArg.temperature ?? dataArg.option;
  return value === undefined ? undefined : String(value);
};

const queryParameters = (valueArg: unknown): Record<string, string> | undefined => {
  if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
    return undefined;
  }
  return Object.fromEntries(Object.entries(valueArg).map(([keyArg, entryValueArg]) => [keyArg, String(entryValueArg)]));
};

const xmlBlocks = (xmlArg: string, tagArg: string): string[] => [...xmlArg.matchAll(new RegExp(`<${tagArg}\\b[^>]*>[\\s\\S]*?<\\/${tagArg}>`, 'gi'))].map((matchArg) => matchArg[0]);
const xmlBlock = (xmlArg: string, tagArg: string): string | undefined => xmlBlocks(xmlArg, tagArg)[0];
const xmlText = (xmlArg: string, tagArg: string): string | undefined => {
  const match = xmlArg.match(new RegExp(`<${tagArg}\\b[^>]*>([\\s\\S]*?)<\\/${tagArg}>`, 'i'));
  return match ? decodeXml(match[1].trim()) : undefined;
};
const stripXml = (xmlArg: string): string => decodeXml(xmlArg.replace(/<[^>]+>/g, '').trim());
const attr = (xmlArg: string, nameArg: string): string | undefined => {
  const match = xmlArg.match(new RegExp(`${nameArg}=["']([^"']*)["']`, 'i'));
  return match ? decodeXml(match[1]) : undefined;
};
const decodeXml = (valueArg: string): string => valueArg.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
const xmlBoolean = (valueArg?: string): boolean | undefined => valueArg === undefined ? undefined : valueArg.toLowerCase() === 'true' || valueArg === '1' || valueArg.toLowerCase() === 'on';
const coerceStatus = (valueArg?: string, uomArg?: string): string | number | boolean | undefined => {
  if (valueArg === undefined || valueArg === '') {
    return undefined;
  }
  if ((uomArg === '2' || uomArg === '78') && Number.isFinite(Number(valueArg))) {
    return Number(valueArg) > 0;
  }
  return Number.isFinite(Number(valueArg)) ? Number(valueArg) : valueArg;
};
const addressFromTarget = (targetArg?: string): string | undefined => targetArg?.match(/isy994[_:.]([a-z0-9_]+)/i)?.[1]?.replace(/_/g, ' ');
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const cleanAttributes = <TValue extends Record<string, unknown>>(valueArg: TValue): TValue => Object.fromEntries(Object.entries(valueArg).filter(([, entryValueArg]) => entryValueArg !== undefined)) as TValue;
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
