import * as plugins from '../../plugins.js';
import { SimpleLocalClient } from '../../core/index.js';
import type { IServiceCallRequest, IServiceCallResult, ISimpleLocalSnapshot } from '../../core/index.js';
import { LoqedMapper, loqedActionForService } from './loqed.mapper.js';
import type { ILoqedConfig, ILoqedStatusData } from './loqed.types.js';
import { loqedProfile } from './loqed.types.js';

const loqedActionIds = {
  open: 1,
  unlock: 2,
  lock: 3,
} as const;

export class LoqedClient extends SimpleLocalClient<ILoqedConfig> {
  private snapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configData: ILoqedConfig) {
    super(loqedProfile, configData);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.snapshot) {
      return clone(this.snapshot);
    }
    if (this.canUseNativeHttp()) {
      try {
        this.snapshot = await this.fetchSnapshot();
        return clone(this.snapshot);
      } catch (errorArg) {
        this.snapshot = LoqedMapper.toSnapshotFromStatus(this.configData, undefined, false, 'runtime', errorMessage(errorArg));
        return clone(this.snapshot);
      }
    }
    this.snapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.snapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.snapshot = 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.configData.commandExecutor || this.configData.client?.execute) {
      return super.execute(requestArg);
    }
    const action = loqedActionForService(requestArg.service, await this.currentBoltState());
    if (!action) {
      return { success: false, error: `Unsupported LOQED service: ${requestArg.service}` };
    }
    if (!this.canUseNativeHttp()) {
      return super.execute(requestArg);
    }
    const secret = this.keySecret();
    const keyId = this.keyLocalId();
    if (!secret || keyId === undefined) {
      return { success: false, error: 'LOQED live control requires config.lockKeySecret and config.lockKeyLocalId from the official local bridge setup.' };
    }
    try {
      const command = this.signedCommand(action, secret, keyId);
      const response = await this.fetchWithTimeout(`${this.baseUrl()}/to_lock?command_signed_base64=${command}`, { method: 'GET' });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`LOQED ${action} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
      }
      this.snapshot = undefined;
      return { success: true, data: { transmitted: true, action, response: text } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const status = await this.getJson<ILoqedStatusData>('status');
    return LoqedMapper.toSnapshotFromStatus(this.configData, status, true, 'http');
  }

  private async currentBoltState(): Promise<string | undefined> {
    const snapshot = await this.getSnapshot(false);
    const rawData = snapshot.rawData;
    return rawData && typeof rawData === 'object' && !Array.isArray(rawData) ? stringValue((rawData as ILoqedStatusData).bolt_state) : undefined;
  }

  private signedCommand(actionArg: keyof typeof loqedActionIds, secretArg: string, keyIdArg: number): string {
    const now = Math.floor(Date.now() / 1000);
    const messageId = Buffer.alloc(8);
    const protocol = Buffer.from([2]);
    const commandType = Buffer.from([7]);
    const timestamp = Buffer.alloc(8);
    timestamp.writeBigUInt64BE(BigInt(now));
    const keyId = Buffer.from([keyIdArg]);
    const deviceId = Buffer.from([1]);
    const action = Buffer.from([loqedActionIds[actionArg]]);
    const hashMessage = Buffer.concat([protocol, commandType, timestamp, keyId, deviceId, action]);
    const hmac = plugins.crypto.createHmac('sha256', Buffer.from(secretArg, 'base64')).update(hashMessage).digest();
    const command = Buffer.concat([messageId, protocol, commandType, timestamp, hmac, keyId, deviceId, action]).toString('base64');
    return encodeURIComponent(command);
  }

  private async getJson<TValue>(pathArg: string): Promise<TValue> {
    const response = await this.fetchWithTimeout(`${this.baseUrl()}/${pathArg.replace(/^\/+/, '')}`, { method: 'GET', headers: { accept: 'application/json, text/plain;q=0.9' } });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`LOQED ${pathArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    return JSON.parse(text) as TValue;
  }

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.configData.timeoutMs || 5000);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private keySecret(): string | undefined {
    return this.configData.lockKeySecret || this.configData.lock_key_key || this.configData.keySecret || this.configData.secret;
  }

  private keyLocalId(): number | undefined {
    return numberValue(this.configData.lockKeyLocalId ?? this.configData.lock_key_local_id ?? this.configData.localKeyId);
  }

  private canUseNativeHttp(): boolean {
    return this.configData.nativeHttpEnabled !== false && Boolean(this.baseUrl(false));
  }

  private baseUrl(requiredArg = true): string {
    if (this.configData.url) {
      return this.configData.url.replace(/\/+$/, '');
    }
    const host = this.configData.host || this.configData.bridgeIp || this.configData.bridge_ip;
    if (!host) {
      if (requiredArg) {
        throw new Error('LOQED native bridge HTTP requires config.url, config.host, or config.bridgeIp.');
      }
      return '';
    }
    if (/^https?:\/\//i.test(host)) {
      return host.replace(/\/+$/, '');
    }
    const port = this.configData.port;
    const portPart = port && !host.includes(':') && port !== 80 ? `:${port}` : '';
    return `${this.configData.useTls ? 'https' : 'http'}://${host}${portPart}`;
  }
}

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

const stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

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