import { GoodweMapper } from './goodwe.mapper.js';
import type { IGoodweClientLike, IGoodweCommandRequest, IGoodweConfig, IGoodweDeviceInfo, IGoodweRawData, IGoodweSnapshot } from './goodwe.types.js';

export class GoodweApiError extends Error {}
export class GoodweApiConnectionError extends GoodweApiError {}
export class GoodweProtocolNotImplementedError extends GoodweApiError {}
export class GoodweUnsupportedCommandError extends GoodweApiError {}

export interface IGoodweRefreshResult {
  success: boolean;
  snapshot: IGoodweSnapshot;
  error?: string;
  data?: unknown;
}

export class GoodweClient {
  private currentSnapshot?: IGoodweSnapshot;

  constructor(private readonly config: IGoodweConfig) {}

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

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = GoodweMapper.toSnapshot({ config: this.config, source: 'snapshot', online: this.config.snapshot.online });
      return this.cloneSnapshot(this.currentSnapshot);
    }

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

    if (!forceRefreshArg && this.hasManualData()) {
      this.currentSnapshot = GoodweMapper.toSnapshot({
        config: this.config,
        rawData: this.config.rawData,
        online: this.config.online ?? true,
        source: 'manual',
      });
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.host) {
      this.currentSnapshot = this.offlineSnapshot('Native GoodWe UDP/TCP transport is not implemented in this TypeScript port. Provide an injected client, commandExecutor, snapshot, or rawData; host-only configs are read-only metadata.');
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('No GoodWe host, injected client, snapshot, or raw data is configured.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IGoodweRefreshResult> {
    try {
      this.currentSnapshot = undefined;
      const liveCapable = Boolean(this.config.client);
      const snapshot = await this.getSnapshot(liveCapable);
      const success = liveCapable && snapshot.online && !snapshot.error;
      return { success, snapshot, error: success ? undefined : snapshot.error || 'GoodWe refresh requires an injected client because native UDP/TCP transport is not implemented.', data: { source: snapshot.source } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: this.cloneSnapshot(snapshot), error };
    }
  }

  public async fetchSnapshot(): Promise<IGoodweSnapshot> {
    throw new GoodweProtocolNotImplementedError('Native GoodWe UDP/TCP snapshot fetching is not implemented. Use config.client, config.snapshot, or config.rawData.');
  }

  public async execute(commandArg: IGoodweCommandRequest): Promise<unknown> {
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }
    if (this.config.client?.execute) {
      return this.config.client.execute(commandArg);
    }
    if (commandArg.action === 'refresh') {
      return (await this.refresh()).snapshot;
    }
    throw new GoodweApiConnectionError('GoodWe commands require an injected client.execute or commandExecutor. Native UDP/TCP writes are not implemented; static snapshots/manual data are read-only.');
  }

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

  private async snapshotFromClient(clientArg: IGoodweClientLike): Promise<IGoodweSnapshot> {
    if (clientArg.getSnapshot) {
      const result = await clientArg.getSnapshot();
      if (this.isSnapshot(result)) {
        return GoodweMapper.toSnapshot({ config: { ...this.config, snapshot: result }, source: 'client', online: result.online });
      }
      return GoodweMapper.toSnapshot({ config: this.config, rawData: result, online: true, source: 'client' });
    }
    if (clientArg.getRawData) {
      return GoodweMapper.toSnapshot({ config: this.config, rawData: await clientArg.getRawData(), online: true, source: 'client' });
    }

    const rawData = await this.rawDataFromClient(clientArg);
    if (!GoodweMapper.hasUsableRawData(rawData)) {
      throw new GoodweApiConnectionError('GoodWe client did not return runtime data, device info, settings, or sensor metadata.');
    }
    return GoodweMapper.toSnapshot({ config: this.config, rawData, online: true, source: 'client' });
  }

  private async rawDataFromClient(clientArg: IGoodweClientLike): Promise<Partial<IGoodweRawData>> {
    const deviceInfo = await this.deviceInfoFromClient(clientArg);
    const runtimeData = await this.callOptional<Record<string, unknown>>(clientArg.readRuntimeData || clientArg.read_runtime_data, clientArg);
    const settingsData = await this.callOptional<Record<string, unknown>>(clientArg.readSettingsData || clientArg.read_settings_data, clientArg).catch(() => undefined);
    const operationMode = await this.callOptional<string | number>(clientArg.getOperationMode || clientArg.get_operation_mode, clientArg).catch(() => undefined);
    const supportedOperationModes = await this.callOptional<Array<string | number>>(clientArg.getOperationModes || clientArg.get_operation_modes, clientArg, false).catch(() => undefined);
    const gridExportLimit = await this.callOptional<number>(clientArg.getGridExportLimit || clientArg.get_grid_export_limit, clientArg).catch(() => undefined);
    const batteryDischargeDepth = await this.callOptional<number>(clientArg.getOngridBatteryDod || clientArg.get_ongrid_battery_dod, clientArg).catch(() => undefined);
    const sensors = typeof clientArg.sensors === 'function' ? GoodweMapper.normalizeSensorDefinitions(clientArg.sensors()) : undefined;
    const settings = typeof clientArg.settings === 'function' ? GoodweMapper.normalizeSettingDefinitions(clientArg.settings()) : undefined;

    return {
      deviceInfo,
      runtimeData,
      settingsData,
      sensors,
      settings,
      operationMode,
      supportedOperationModes,
      gridExportLimit,
      batteryDischargeDepth,
      fetchedAt: new Date().toISOString(),
    };
  }

  private async deviceInfoFromClient(clientArg: IGoodweClientLike): Promise<Partial<IGoodweDeviceInfo> | undefined> {
    const methodResult = await this.callOptional<Partial<IGoodweDeviceInfo>>(clientArg.getDeviceInfo || clientArg.readDeviceInfo || clientArg.read_device_info, clientArg).catch(() => undefined);
    const propertyInfo = GoodweMapper.deviceInfoFromRecord(clientArg as Record<string, unknown>);
    return { ...propertyInfo, ...methodResult };
  }

  private async callOptional<TValue>(methodArg: unknown, thisArg: unknown, ...args: unknown[]): Promise<TValue | undefined> {
    if (typeof methodArg !== 'function') {
      return undefined;
    }
    return await (methodArg as (...args: unknown[]) => Promise<TValue> | TValue).apply(thisArg, args);
  }

  private offlineSnapshot(errorArg: string): IGoodweSnapshot {
    return GoodweMapper.toSnapshot({ config: this.config, rawData: this.config.rawData, online: false, source: 'runtime', error: errorArg });
  }

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

  private isSnapshot(valueArg: unknown): valueArg is IGoodweSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'inverter' in valueArg && 'runtimeData' in valueArg && 'sensors' in valueArg);
  }

  private cloneSnapshot(snapshotArg: IGoodweSnapshot): IGoodweSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IGoodweSnapshot;
  }

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