import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { BaseIntegration } from '../../core/classes.baseintegration.js';
import type { IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
import { ZwaveJsClient } from './zwave_js.classes.client.js';
import { ZwaveJsConfigFlow } from './zwave_js.classes.configflow.js';
import { createZwaveJsDiscoveryDescriptor } from './zwave_js.discovery.js';
import { ZwaveJsMapper } from './zwave_js.mapper.js';
import type { IZwaveJsConfig, IZwaveJsServerCommand } from './zwave_js.types.js';

export class ZwaveJsIntegration extends BaseIntegration<IZwaveJsConfig> {
  public readonly domain = 'zwave_js';
  public readonly displayName = 'Z-Wave';
  public readonly status = 'control-runtime' as const;
  public readonly discoveryDescriptor = createZwaveJsDiscoveryDescriptor();
  public readonly configFlow = new ZwaveJsConfigFlow();
  public readonly metadata = {
    source: 'home-assistant/core',
    upstreamPath: 'homeassistant/components/zwave_js',
    upstreamDomain: 'zwave_js',
    integrationType: 'hub',
    iotClass: 'local_push',
    qualityScale: 'unknown',
  };

  public async setup(configArg: IZwaveJsConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
    void contextArg;
    return new ZwaveJsRuntime(new ZwaveJsClient(configArg));
  }

  public async destroy(): Promise<void> {}
}

export class HomeAssistantZwaveJsIntegration extends ZwaveJsIntegration {}

class ZwaveJsRuntime implements IIntegrationRuntime {
  public domain = 'zwave_js';

  constructor(private readonly client: ZwaveJsClient) {}

  public async devices(): Promise<shxInterfaces.data.IDeviceDefinition[]> {
    return ZwaveJsMapper.toDevices(await this.client.getSnapshot());
  }

  public async entities(): Promise<IIntegrationEntity[]> {
    return ZwaveJsMapper.toEntities(await this.client.getSnapshot());
  }

  public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void): Promise<() => Promise<void>> {
    const unsubscribe = this.client.onEvent((eventArg) => {
      handlerArg({
        type: eventArg.event === 'value updated' ? 'state_changed' : 'device_added',
        integrationDomain: 'zwave_js',
        deviceId: typeof eventArg.nodeId === 'number' ? `zwave_js.node.${eventArg.nodeId}` : undefined,
        data: eventArg,
        timestamp: Date.now(),
      });
    });
    await this.client.getSnapshot();
    return async () => unsubscribe();
  }

  public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (requestArg.domain === 'zwave_js') {
      const command = this.commandFromService(requestArg);
      if (!command) {
        return { success: false, error: `Unsupported Z-Wave JS service: ${requestArg.service}` };
      }
      const data = await this.client.sendCommand(command);
      return { success: true, data };
    }

    const command = ZwaveJsMapper.commandForService(await this.client.getSnapshot(), requestArg);
    if (!command) {
      return { success: false, error: `Z-Wave JS entity service ${requestArg.domain}.${requestArg.service} has no writable value mapping.` };
    }
    const data = await this.client.sendCommand(command);
    return { success: true, data };
  }

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

  private commandFromService(requestArg: IServiceCallRequest): IZwaveJsServerCommand | undefined {
    if (requestArg.service === 'ping') {
      const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
      return nodeId ? { command: 'node.ping', nodeId } : undefined;
    }
    if (requestArg.service === 'refresh_value') {
      const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
      const valueId = requestArg.data?.value_id ?? requestArg.data?.valueId;
      return nodeId && this.isRecord(valueId) ? { command: 'node.poll_value', nodeId, valueId } : undefined;
    }
    if (requestArg.service === 'set_value') {
      const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
      const valueId = requestArg.data?.value_id ?? requestArg.data?.valueId;
      return nodeId && this.isRecord(valueId) ? { command: 'node.set_value', nodeId, valueId, value: requestArg.data?.value } : undefined;
    }
    if (requestArg.service === 'set_config_parameter') {
      const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
      const parameter = this.numberValue(requestArg.data?.parameter);
      return nodeId && parameter ? { command: 'node.set_raw_config_parameter_value', nodeId, parameter, bitMask: requestArg.data?.bitmask, value: requestArg.data?.value, valueSize: requestArg.data?.value_size, valueFormat: requestArg.data?.value_format } : undefined;
    }
    if (requestArg.service === 'begin_inclusion') {
      return { command: 'controller.begin_inclusion', strategy: requestArg.data?.strategy || 0 };
    }
    if (requestArg.service === 'stop_inclusion') {
      return { command: 'controller.stop_inclusion' };
    }
    if (requestArg.service === 'invoke_cc_api') {
      const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
      const commandClass = this.numberValue(requestArg.data?.command_class ?? requestArg.data?.commandClass);
      const methodName = requestArg.data?.method_name ?? requestArg.data?.methodName;
      return nodeId && commandClass && typeof methodName === 'string' ? { command: 'endpoint.invoke_cc_api', nodeId, endpoint: requestArg.data?.endpoint, commandClass, methodName, args: Array.isArray(requestArg.data?.parameters) ? requestArg.data.parameters : [] } : undefined;
    }
    return undefined;
  }

  private numberValue(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
  }

  private isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
    return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
  }
}
