import { SimpleLocalMapper, type ISimpleLocalClientLike, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import type { INx584Config, INx584Partition, INx584Snapshot, INx584Zone, TNx584SnapshotSource } from './nx584.types.js';
import { nx584DefaultName, nx584DefaultPort, nx584DefaultTimeoutMs, nx584Profile } from './nx584.types.js';

export class Nx584Client {
  private currentSnapshot?: INx584Snapshot;

  constructor(private readonly config: INx584Config) {}

  public async getSnapshot(forceRefreshArg = false): Promise<INx584Snapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return this.clone(this.currentSnapshot);
    }

    this.currentSnapshot = await this.readSnapshot(forceRefreshArg);
    return this.clone(this.currentSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentSnapshot = 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.baseUrl()) {
      return this.executeInjectedOrUnsupported(requestArg);
    }

    try {
      if (requestArg.service === 'bypass_zone' || requestArg.service === 'unbypass_zone') {
        const zone = this.numberValue(requestArg.data?.zone);
        if (zone === undefined) {
          return { success: false, error: `NX584 ${requestArg.service} requires data.zone.` };
        }
        const bypassed = requestArg.service === 'bypass_zone';
        await this.request(`/zones/${zone}`, {
          method: 'PUT',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ bypassed }),
        });
        this.currentSnapshot = undefined;
        return { success: true, data: { zone, bypassed } };
      }

      if (requestArg.service === 'alarm_arm_home' || requestArg.service === 'alarm_arm_away') {
        const type = requestArg.service === 'alarm_arm_home' ? 'stay' : 'exit';
        const partition = this.numberValue(requestArg.data?.partition) || 1;
        await this.request(`/command?cmd=arm&type=${encodeURIComponent(type)}&partition=${partition}`);
        this.currentSnapshot = undefined;
        return { success: true, data: { command: 'arm', type, partition } };
      }

      if (requestArg.service === 'alarm_disarm') {
        const code = this.stringValue(requestArg.data?.code) || this.stringValue(requestArg.data?.master_pin) || this.stringValue(requestArg.data?.masterPin);
        if (!code) {
          return { success: false, error: 'NX584 alarm_disarm requires data.code or data.master_pin.' };
        }
        const partition = this.numberValue(requestArg.data?.partition) || 1;
        await this.request(`/command?cmd=disarm&master_pin=${encodeURIComponent(code)}&partition=${partition}`);
        this.currentSnapshot = undefined;
        return { success: true, data: { command: 'disarm', partition } };
      }

      return this.executeInjectedOrUnsupported(requestArg);
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

  public async ping(): Promise<boolean> {
    const snapshot = await this.getSnapshot(true);
    return snapshot.online && !snapshot.error;
  }

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

  private async readSnapshot(forceRefreshArg: boolean): Promise<INx584Snapshot> {
    if (!forceRefreshArg && this.config.snapshot) {
      return this.snapshotFromSimpleLocal('snapshot');
    }

    if (this.config.client) {
      try {
        return await this.snapshotFromClient(this.config.client);
      } catch (errorArg) {
        return this.offlineSnapshot(this.errorMessage(errorArg));
      }
    }

    if (this.config.snapshotProvider) {
      try {
        const provided = await this.config.snapshotProvider();
        if (provided !== undefined) {
          return SimpleLocalMapper.toSnapshot({ profile: nx584Profile, config: this.config, rawData: provided, online: true, source: 'provider' }) as INx584Snapshot;
        }
      } catch (errorArg) {
        return this.offlineSnapshot(this.errorMessage(errorArg));
      }
    }

    if (this.config.rawData !== undefined || this.config.rawText !== undefined || this.config.entities || this.config.snapshot) {
      return this.snapshotFromSimpleLocal(this.config.snapshot ? 'snapshot' : 'manual');
    }

    if (this.baseUrl()) {
      try {
        return await this.fetchSnapshot();
      } catch (errorArg) {
        return this.offlineSnapshot(this.errorMessage(errorArg));
      }
    }

    return this.offlineSnapshot('NX584 live snapshots require config.host/config.url for the pynx584 HTTP bridge, snapshot/manual data, snapshotProvider, or an injected client.');
  }

  private async fetchSnapshot(): Promise<INx584Snapshot> {
    const zonesResponse = await this.requestJson('/zones') as { zones?: unknown };
    const partitionsResponse = await this.requestJson('/partitions') as { partitions?: unknown };
    const zones = this.zonesFromValue(zonesResponse.zones).filter((zoneArg) => !this.excludedZones().has(zoneArg.number));
    const partitions = this.partitionsFromValue(partitionsResponse.partitions);
    const version = await this.version();
    return this.snapshotFromBridge(zones, partitions, version, true, 'http');
  }

  private async snapshotFromClient(clientArg: ISimpleLocalClientLike): Promise<INx584Snapshot> {
    const value = clientArg.getSnapshot ? await clientArg.getSnapshot() : clientArg.getState ? await clientArg.getState() : clientArg.getRawData ? await clientArg.getRawData() : undefined;
    if (value === undefined) {
      throw new Error('NX584 injected client must expose getSnapshot(), getState(), or getRawData().');
    }
    return SimpleLocalMapper.toSnapshot({ profile: nx584Profile, config: this.config, rawData: value, online: true, source: 'client' }) as INx584Snapshot;
  }

  private snapshotFromSimpleLocal(sourceArg: TNx584SnapshotSource): INx584Snapshot {
    return SimpleLocalMapper.toSnapshot({
      profile: nx584Profile,
      config: this.config,
      rawData: this.config.rawData ?? this.config.rawText,
      online: this.config.online ?? this.config.snapshot?.online ?? true,
      source: sourceArg,
    }) as INx584Snapshot;
  }

  private snapshotFromBridge(zonesArg: INx584Zone[], partitionsArg: INx584Partition[], versionArg: string | undefined, onlineArg: boolean, sourceArg: TNx584SnapshotSource): INx584Snapshot {
    const endpoint = this.endpointInfo();
    const alarmState = this.alarmState(partitionsArg[0], zonesArg);
    return {
      device: {
        id: this.config.uniqueId || this.config.deviceId || (endpoint ? `${endpoint.host}:${endpoint.port}` : undefined) || this.config.name || nx584Profile.domain,
        name: this.config.name || nx584DefaultName,
        manufacturer: nx584Profile.manufacturer,
        model: nx584Profile.model,
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: endpoint?.tls ? 'https' : 'http',
        configurationUrl: this.baseUrl(),
        attributes: { version: versionArg },
      },
      entities: this.entitiesFromBridge(zonesArg, partitionsArg, alarmState, onlineArg),
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: sourceArg,
      rawData: { zones: zonesArg, partitions: partitionsArg, version: versionArg },
      capabilities: {
        localRead: true,
        localControl: Boolean(this.baseUrl()),
        bridge: 'pynx584',
      },
      zones: zonesArg,
      partitions: partitionsArg,
      version: versionArg,
      alarmState,
    };
  }

  private entitiesFromBridge(zonesArg: INx584Zone[], partitionsArg: INx584Partition[], alarmStateArg: string, onlineArg: boolean): ISimpleLocalEntitySnapshot[] {
    const entities: ISimpleLocalEntitySnapshot[] = [{
      id: 'alarm_state',
      name: 'Alarm State',
      platform: 'sensor',
      state: alarmStateArg,
      available: onlineArg,
      attributes: { partitions: partitionsArg.length },
    }];

    for (const zone of zonesArg) {
      entities.push({
        id: `zone_${zone.number}`,
        name: zone.name || `Zone ${zone.number}`,
        platform: 'binary_sensor',
        state: Boolean(zone.state),
        available: onlineArg,
        deviceClass: this.zoneDeviceClass(zone.number),
        attributes: {
          zoneNumber: zone.number,
          bypassed: Boolean(zone.bypassed),
        },
      });
    }

    return entities;
  }

  private alarmState(partitionArg: INx584Partition | undefined, zonesArg: INx584Zone[]): string {
    if (!partitionArg) {
      return 'unknown';
    }
    const flags = this.partitionFlags(partitionArg);
    if (flags.includes('Siren on')) {
      return 'triggered';
    }
    if (!partitionArg.armed) {
      return 'disarmed';
    }
    if (zonesArg.some((zoneArg) => zoneArg.bypassed)) {
      return 'armed_home';
    }
    return 'armed_away';
  }

  private async version(): Promise<string | undefined> {
    const response = await this.request('/version', undefined, true);
    if (response.status === 404) {
      return '1.0';
    }
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`NX584 /version failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    if (!text) {
      return undefined;
    }
    try {
      const parsed = JSON.parse(text) as Record<string, unknown>;
      return this.stringValue(parsed.version) || text;
    } catch {
      return text;
    }
  }

  private async requestJson(pathArg: string): Promise<unknown> {
    const response = await this.request(pathArg);
    return await response.json();
  }

  private async request(pathArg: string, initArg?: RequestInit, allowNotFoundArg = false): Promise<Response> {
    const baseUrl = this.baseUrl();
    if (!baseUrl) {
      throw new Error('NX584 HTTP bridge request requires config.host/config.url.');
    }
    const response = await globalThis.fetch(`${baseUrl}${pathArg}`, {
      ...initArg,
      signal: AbortSignal.timeout(this.config.timeoutMs || nx584DefaultTimeoutMs),
    });
    if (!response.ok && !(allowNotFoundArg && response.status === 404)) {
      const text = await response.text().catch(() => '');
      throw new Error(`NX584 HTTP bridge request ${pathArg} failed with HTTP ${response.status}${text ? `: ${text.slice(0, 160)}` : ''}`);
    }
    return response;
  }

  private zonesFromValue(valueArg: unknown): INx584Zone[] {
    if (!Array.isArray(valueArg)) {
      return [];
    }
    return valueArg.map((zoneArg, indexArg) => {
      const zone = zoneArg && typeof zoneArg === 'object' ? zoneArg as Record<string, unknown> : {};
      const number = this.numberValue(zone.number) || indexArg + 1;
      return {
        ...zone,
        number,
        name: this.stringValue(zone.name) || `Zone ${number}`,
        state: Boolean(zone.state),
        bypassed: Boolean(zone.bypassed),
      };
    });
  }

  private partitionsFromValue(valueArg: unknown): INx584Partition[] {
    if (!Array.isArray(valueArg)) {
      return [];
    }
    return valueArg.map((partitionArg, indexArg) => {
      const partition = partitionArg && typeof partitionArg === 'object' ? partitionArg as Record<string, unknown> : {};
      return {
        ...partition,
        number: this.numberValue(partition.number) || indexArg + 1,
        armed: Boolean(partition.armed),
        condition_flags: this.stringArray(partition.condition_flags ?? partition.conditionFlags),
      };
    });
  }

  private partitionFlags(partitionArg: INx584Partition): string[] {
    return this.stringArray(partitionArg.condition_flags ?? partitionArg.conditionFlags);
  }

  private zoneDeviceClass(zoneNumberArg: number): string {
    const zoneTypes = this.config.zoneTypes || this.config.zone_types || {};
    return this.stringValue(zoneTypes[zoneNumberArg]) || this.stringValue(zoneTypes[String(zoneNumberArg)]) || 'opening';
  }

  private excludedZones(): Set<number> {
    const values = this.config.excludeZones || this.config.exclude_zones || [];
    return new Set(values.map((valueArg) => this.numberValue(valueArg)).filter((valueArg): valueArg is number => valueArg !== undefined));
  }

  private async executeInjectedOrUnsupported(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.config.client?.execute) {
      return normalizeServiceResult(await this.config.client.execute(requestArg));
    }
    if (this.config.commandExecutor) {
      return normalizeServiceResult(await this.config.commandExecutor.execute(requestArg));
    }
    return {
      success: false,
      error: `NX584 service ${requestArg.service} requires an injected client.execute() or commandExecutor when no pynx584 HTTP bridge host/url is configured; no live control is reported without a real executor or bridge request.`,
    };
  }

  private offlineSnapshot(errorArg: string): INx584Snapshot {
    const endpoint = this.endpointInfo();
    return {
      device: {
        id: this.config.uniqueId || (endpoint ? `${endpoint.host}:${endpoint.port}` : undefined) || this.config.name || nx584Profile.domain,
        name: this.config.name || nx584DefaultName,
        manufacturer: nx584Profile.manufacturer,
        model: nx584Profile.model,
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: endpoint?.tls ? 'https' : endpoint ? 'http' : 'unknown',
      },
      entities: [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: false, available: true, attributes: { source: 'runtime' } }],
      online: false,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
      capabilities: { localRead: true, localControl: false },
      zones: [],
      partitions: [],
      alarmState: 'unknown',
    };
  }

  private baseUrl(): string | undefined {
    if (this.config.url) {
      return this.config.url.replace(/\/+$/, '');
    }
    const host = this.config.host;
    if (!host) {
      return undefined;
    }
    const tls = this.config.useTls || this.config.ssl || this.config.tls;
    const port = this.config.port || nx584DefaultPort;
    return `${tls ? 'https' : 'http'}://${this.hostForUrl(host)}:${port}`;
  }

  private endpointInfo(): { host: string; port: number; tls: boolean } | undefined {
    if (this.config.host) {
      return { host: this.config.host, port: this.config.port || nx584DefaultPort, tls: Boolean(this.config.useTls || this.config.ssl || this.config.tls) };
    }
    if (!this.config.url) {
      return undefined;
    }
    try {
      const url = new URL(this.config.url);
      return { host: url.hostname, port: Number(url.port) || this.config.port || nx584DefaultPort, tls: url.protocol === 'https:' };
    } catch {
      return undefined;
    }
  }

  private hostForUrl(hostArg: string): string {
    return hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
  }

  private stringArray(valueArg: unknown): string[] {
    if (!Array.isArray(valueArg)) {
      return [];
    }
    return valueArg.filter((itemArg): itemArg is string => typeof itemArg === 'string');
  }

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

  private stringValue(valueArg: unknown): string | undefined {
    return typeof valueArg === 'string' && valueArg.length ? valueArg : undefined;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }

  private clone<TValue>(valueArg: TValue): TValue {
    return JSON.parse(JSON.stringify(valueArg)) as TValue;
  }
}

const normalizeServiceResult = (valueArg: IServiceCallResult | unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg) {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};
