import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { MedcomBleMapper } from './medcom_ble.mapper.js';
import type { IMedcomBleConfig } from './medcom_ble.types.js';
import { medcomBleProfile } from './medcom_ble.types.js';

export class MedcomBleUnsupportedTransportError extends Error {}

export class MedcomBleClient extends SimpleLocalClient<IMedcomBleConfig> {
  private currentMedcomSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IMedcomBleConfig) {
    super(medcomBleProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.currentMedcomSnapshot) {
      return clone(this.currentMedcomSnapshot);
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      this.currentMedcomSnapshot = MedcomBleMapper.toSnapshot({ config: this.configArg, source: 'snapshot', online: this.configArg.snapshot.online });
      return clone(this.currentMedcomSnapshot);
    }
    if (this.configArg.client) {
      try {
        this.currentMedcomSnapshot = await this.snapshotFromMedcomClient();
      } catch (errorArg) {
        this.currentMedcomSnapshot = this.unsupportedSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentMedcomSnapshot);
    }
    if (this.configArg.snapshotProvider) {
      try {
        const provided = await this.configArg.snapshotProvider();
        if (provided !== undefined) {
          this.currentMedcomSnapshot = MedcomBleMapper.toSnapshot({ config: this.configArg, rawData: provided, source: 'provider', online: true });
          return clone(this.currentMedcomSnapshot);
        }
      } catch (errorArg) {
        this.currentMedcomSnapshot = this.unsupportedSnapshot(errorMessage(errorArg));
        return clone(this.currentMedcomSnapshot);
      }
    }
    if (!forceRefreshArg && this.hasOfflineData()) {
      this.currentMedcomSnapshot = MedcomBleMapper.toSnapshot({ config: this.configArg, rawData: this.configArg.rawData ?? this.configArg.rawText, source: 'manual', online: this.configArg.online ?? true });
      return clone(this.currentMedcomSnapshot);
    }

    this.currentMedcomSnapshot = this.unsupportedSnapshot('Medcom BLE live polling requires a BLE stack compatible with bleak/bleak-retry-connector. The Home Assistant source exposes only BLE GATT reads/notifications, not HTTP/TCP/TLS/file polling.');
    return clone(this.currentMedcomSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentMedcomSnapshot = undefined;
    const liveCapable = Boolean(this.configArg.client || this.configArg.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error || 'Medcom BLE refresh requires an injected BLE client or snapshotProvider.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.commandExecutor) {
      return normalizeServiceResult(await this.configArg.commandExecutor.execute(requestArg));
    }
    if (this.configArg.client?.execute) {
      return normalizeServiceResult(await this.configArg.client.execute(requestArg));
    }
    return { success: false, error: 'Medcom BLE is read-only in Home Assistant and this TypeScript port does not fake BLE writes or commands.' };
  }

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

  private async snapshotFromMedcomClient(): Promise<ISimpleLocalSnapshot> {
    const client = this.configArg.client;
    if (!client) {
      throw new MedcomBleUnsupportedTransportError('No Medcom BLE client configured.');
    }
    if (client.getSnapshot) {
      const result = await client.getSnapshot();
      if (isSimpleSnapshot(result)) {
        return MedcomBleMapper.toSnapshot({ config: { ...this.configArg, snapshot: result }, source: 'client', online: result.online });
      }
      return MedcomBleMapper.toSnapshot({ config: this.configArg, rawData: result, source: 'client', online: true });
    }
    if (client.getRawData) {
      return MedcomBleMapper.toSnapshot({ config: this.configArg, rawData: await client.getRawData(), source: 'client', online: true });
    }
    if (client.getState) {
      return MedcomBleMapper.toSnapshot({ config: this.configArg, rawData: await client.getState(), source: 'client', online: true });
    }
    throw new MedcomBleUnsupportedTransportError('Medcom BLE client must expose getSnapshot(), getRawData(), or getState().');
  }

  private hasOfflineData(): boolean {
    return Boolean(this.configArg.snapshot || this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length);
  }

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

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

const isSimpleSnapshot = (valueArg: unknown): valueArg is ISimpleLocalSnapshot => Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg);
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);
