import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { NukiMapper } from './nuki.mapper.js';
import type { INukiApiData, INukiConfig } from './nuki.types.js';
import { nukiDefaultPort, nukiDefaultTimeoutMs, nukiProfile } from './nuki.types.js';

export class NukiApiError extends Error {}
export class NukiApiConnectionError extends NukiApiError {}
export class NukiApiAuthenticationError extends NukiApiError {}
export class NukiApiUnsupportedError extends NukiApiError {}

const actionLockUnlock = 1;
const actionLockLock = 2;
const actionLockUnlatch = 3;
const actionLockNGo = 4;
const actionLockNGoUnlatch = 5;
const actionOpenerActivateRto = 1;
const actionOpenerDeactivateRto = 2;
const actionOpenerElectricStrike = 3;
const actionOpenerActivateContinuous = 4;
const actionOpenerDeactivateContinuous = 5;

export class NukiClient extends SimpleLocalClient<INukiConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(configArg: INukiConfig) {
    super(nukiProfile, configArg);
    this.configArg = configArg;
  }

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

    if (!forceRefreshArg && (this.configArg.client || this.configArg.snapshotProvider || this.hasManualData())) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return this.clone(this.cachedSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        this.cachedSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.cachedSnapshot = this.offlineNukiSnapshot(this.errorMessage(errorArg));
      }
      return this.clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return this.clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(Boolean(this.hasEndpoint()));
    const success = snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error || 'Nuki Bridge refresh requires a live host/url plus token, injected client, snapshotProvider, snapshot, or rawData.', data: snapshot };
  }

  public async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const rawData = await this.fetchRawData();
    return NukiMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'http' });
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.hasEndpoint()) {
      return super.execute(requestArg);
    }

    try {
      const data = await this.executeNative(requestArg);
      this.cachedSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

  private readonly configArg: INukiConfig;

  private async fetchRawData(): Promise<INukiApiData> {
    const [infoResponse, devices] = await Promise.all([
      this.requestJson('info'),
      this.requestJson('list'),
    ]);
    if (Array.isArray(infoResponse)) {
      throw new NukiApiConnectionError('Nuki Bridge /info response is not an object.');
    }
    if (!Array.isArray(devices)) {
      throw new NukiApiConnectionError('Nuki Bridge /list response is not an array.');
    }
    return { info: infoResponse, devices: devices.filter((deviceArg): deviceArg is Record<string, unknown> => Boolean(deviceArg && typeof deviceArg === 'object' && !Array.isArray(deviceArg))), fetchedAt: new Date().toISOString() };
  }

  private async executeNative(requestArg: IServiceCallRequest): Promise<unknown> {
    const devices = await this.currentDevices();
    const device = this.targetDevice(devices, requestArg);
    const deviceType = this.numberValue(requestArg.data?.deviceType) ?? this.numberValue(device.deviceType) ?? 0;
    const nukiId = this.numberValue(requestArg.data?.nukiId) ?? this.numberValue(device.nukiId);
    if (nukiId === undefined) {
      throw new NukiApiUnsupportedError('Nuki service call requires data.nukiId or a target entity from the current bridge /list response.');
    }
    const action = this.actionForService(requestArg, device, deviceType);
    return this.requestJson('lockAction', {
      nukiId: String(nukiId),
      deviceType: String(deviceType),
      action: String(action),
      noWait: '1',
    });
  }

  private actionForService(requestArg: IServiceCallRequest, deviceArg: Record<string, unknown>, deviceTypeArg: number): number {
    const service = requestArg.service;
    const state = this.numberValue(this.record(deviceArg.lastKnownState).state ?? deviceArg.state);
    const mode = this.numberValue(this.record(deviceArg.lastKnownState).mode ?? deviceArg.mode);
    const opener = deviceTypeArg === 2;
    if (service === 'turn_on' || service === 'unlock') {
      return opener ? actionOpenerActivateRto : actionLockUnlock;
    }
    if (service === 'turn_off' || service === 'lock') {
      return opener ? actionOpenerDeactivateRto : actionLockLock;
    }
    if (service === 'open') {
      return opener ? actionOpenerElectricStrike : actionLockUnlatch;
    }
    if (service === 'toggle') {
      if (opener) {
        return state === 3 || mode === 3 ? actionOpenerDeactivateRto : actionOpenerActivateRto;
      }
      return state === 1 ? actionLockUnlock : actionLockLock;
    }
    if (service === 'lock_n_go') {
      if (opener) {
        throw new NukiApiUnsupportedError('Nuki lock_n_go applies to lock devices, not openers.');
      }
      return requestArg.data?.unlatch === true ? actionLockNGoUnlatch : actionLockNGo;
    }
    if (service === 'set_continuous_mode') {
      if (!opener) {
        throw new NukiApiUnsupportedError('Nuki continuous mode applies to opener devices only.');
      }
      return requestArg.data?.enabled === true || requestArg.data?.enable === true ? actionOpenerActivateContinuous : actionOpenerDeactivateContinuous;
    }
    throw new NukiApiUnsupportedError(`Unsupported Nuki service: ${requestArg.domain}.${service}`);
  }

  private async currentDevices(): Promise<Array<Record<string, unknown>>> {
    if (this.cachedSnapshot?.rawData && typeof this.cachedSnapshot.rawData === 'object' && !Array.isArray(this.cachedSnapshot.rawData)) {
      const devices = (this.cachedSnapshot.rawData as INukiApiData).devices;
      if (Array.isArray(devices)) {
        return devices;
      }
    }
    const rawData = await this.fetchRawData();
    return rawData.devices || [];
  }

  private targetDevice(devicesArg: Array<Record<string, unknown>>, requestArg: IServiceCallRequest): Record<string, unknown> {
    const requestedId = this.numberValue(requestArg.data?.nukiId) ?? this.nukiIdFromTarget(requestArg.target.entityId || requestArg.target.deviceId);
    if (requestedId !== undefined) {
      const found = devicesArg.find((deviceArg) => this.numberValue(deviceArg.nukiId) === requestedId);
      if (found) {
        return found;
      }
    }
    const requestedType = this.numberValue(requestArg.data?.deviceType);
    if (requestArg.service === 'set_continuous_mode') {
      const opener = devicesArg.find((deviceArg) => this.numberValue(deviceArg.deviceType) === 2);
      if (opener) {
        return opener;
      }
    }
    if (requestedType !== undefined) {
      const found = devicesArg.find((deviceArg) => this.numberValue(deviceArg.deviceType) === requestedType);
      if (found) {
        return found;
      }
    }
    if (devicesArg[0]) {
      return devicesArg[0];
    }
    throw new NukiApiUnsupportedError('Nuki Bridge /list returned no devices to target.');
  }

  private async requestJson(endpointArg: string, paramsArg: Record<string, string> = {}): Promise<Record<string, unknown> | Array<Record<string, unknown>>> {
    if (this.configArg.secure === true || this.configArg.encryptToken === true) {
      throw new NukiApiUnsupportedError('Encrypted Nuki ctoken/nonce authentication requires PyNaCl/libsodium SecretBox (XSalsa20-Poly1305), which is not available through existing dependencies. Configure encryptToken:false/plain token or inject a native client.');
    }
    if (!this.configArg.token) {
      throw new NukiApiAuthenticationError('Nuki Bridge HTTP API requires config.token for live local requests.');
    }

    const url = new URL(`${this.baseUrl()}/${endpointArg}`);
    url.searchParams.set('token', this.configArg.token);
    for (const [key, value] of Object.entries(paramsArg)) {
      url.searchParams.set(key, value);
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.configArg.timeoutMs || nukiDefaultTimeoutMs);
    try {
      const response = await globalThis.fetch(url, { method: 'GET', headers: { accept: 'application/json' }, signal: controller.signal });
      const text = await response.text();
      if (response.status === 401 || response.status === 403) {
        throw new NukiApiAuthenticationError(`Nuki Bridge authentication failed with HTTP ${response.status}.`);
      }
      if (!response.ok) {
        throw new NukiApiConnectionError(`Nuki Bridge endpoint ${url.toString()} failed with HTTP ${response.status}${text ? `: ${text.slice(0, 160)}` : ''}`);
      }
      const parsed = text ? JSON.parse(text) as unknown : {};
      if (Array.isArray(parsed)) {
        return parsed.filter((itemArg): itemArg is Record<string, unknown> => Boolean(itemArg && typeof itemArg === 'object' && !Array.isArray(itemArg)));
      }
      if (!parsed || typeof parsed !== 'object') {
        throw new Error(`unexpected ${typeof parsed}`);
      }
      const data = parsed as Record<string, unknown>;
      if (data.success === false) {
        throw new NukiApiConnectionError(`Nuki Bridge returned success:false for ${endpointArg}.`);
      }
      return data;
    } catch (errorArg) {
      if (errorArg instanceof NukiApiError) {
        throw errorArg;
      }
      throw new NukiApiConnectionError(`Connection to ${url.toString()} failed: ${this.errorMessage(errorArg)}`);
    } finally {
      clearTimeout(timeout);
    }
  }

  private baseUrl(): string {
    if (this.configArg.url) {
      const url = new URL(this.configArg.url.includes('://') ? this.configArg.url : `http://${this.configArg.url}`);
      return url.origin;
    }
    const host = this.configArg.host;
    if (!host) {
      throw new NukiApiConnectionError('Nuki Bridge local HTTP API requires config.host or config.url.');
    }
    if (host.includes('://')) {
      return new URL(host).origin;
    }
    const port = this.configArg.port || nukiDefaultPort;
    return `http://${this.hostForUrl(host)}:${port}`;
  }

  private nukiIdFromTarget(valueArg: string | undefined): number | undefined {
    if (!valueArg) {
      return undefined;
    }
    const match = valueArg.match(/(?:device_|nuki_|_)(\d{3,})(?:\D|$)/);
    return match ? Number(match[1]) : undefined;
  }

  private hasEndpoint(): boolean {
    return Boolean((this.configArg.host || this.configArg.url) && this.configArg.token);
  }

  private hasManualData(): boolean {
    return Boolean(this.configArg.snapshot || this.configArg.rawData || this.configArg.rawText || this.configArg.entities);
  }

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

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

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

  private offlineNukiSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return NukiMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorArg });
  }

  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;
  }
}
