import { HomeWizardMapper } from './homewizard.mapper.js';
import type {
  IHomeWizardCommandRequest,
  IHomeWizardConfig,
  IHomeWizardRawData,
  IHomeWizardRefreshResult,
  IHomeWizardSnapshot,
  THomeWizardApiVersion,
  THomeWizardProtocol,
} from './homewizard.types.js';
import { homeWizardDefaultHttpPort, homeWizardDefaultHttpsPort, homeWizardDefaultTimeoutMs } from './homewizard.types.js';

export class HomeWizardApiError extends Error {}
export class HomeWizardApiConnectionError extends HomeWizardApiError {}
export class HomeWizardApiAuthorizationError extends HomeWizardApiError {}
export class HomeWizardApiDisabledError extends HomeWizardApiError {}
export class HomeWizardUnsupportedCommandError extends HomeWizardApiError {}

interface IHomeWizardEndpoint {
  protocol: THomeWizardProtocol;
  host: string;
  port: number;
}

interface IHomeWizardHttpResponse {
  status: number;
  text: string;
}

export class HomeWizardClient {
  private currentSnapshot?: IHomeWizardSnapshot;

  constructor(private readonly config: IHomeWizardConfig) {}

  public static async requestToken(hostArg: string, optionsArg: { port?: number; protocol?: THomeWizardProtocol; name?: string; timeoutMs?: number; verifyTls?: boolean } = {}): Promise<string | undefined> {
    const client = new HomeWizardClient({
      host: hostArg,
      port: optionsArg.port,
      protocol: optionsArg.protocol || 'https',
      apiVersion: 'v2',
      timeoutMs: optionsArg.timeoutMs,
      verifyTls: optionsArg.verifyTls,
    });
    return client.requestToken(optionsArg.name);
  }

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

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

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

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

    if (this.hasEndpoint()) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return HomeWizardMapper.clone(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('No HomeWizard local endpoint, injected client, snapshot, or raw data is configured.');
    return HomeWizardMapper.clone(this.currentSnapshot);
  }

  public async refresh(): Promise<IHomeWizardRefreshResult> {
    try {
      this.currentSnapshot = undefined;
      const liveCapable = Boolean(this.config.client || this.hasEndpoint());
      const snapshot = await this.getSnapshot(liveCapable);
      const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
      return { success, snapshot, error: success ? undefined : snapshot.error || 'HomeWizard refresh requires a live host or injected client.', data: { source: snapshot.source } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: HomeWizardMapper.clone(snapshot), error };
    }
  }

  public async fetchSnapshot(): Promise<IHomeWizardSnapshot> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new HomeWizardApiConnectionError('HomeWizard local API snapshot requires config.host or config.url.');
    }

    const apiVersion = this.effectiveApiVersion(endpoint);
    if (apiVersion === 'v2' && !this.accessToken()) {
      throw new HomeWizardApiAuthorizationError('HomeWizard API v2 snapshots require config.token or config.authorizationToken.');
    }

    const rawData = apiVersion === 'v2' ? await this.fetchV2RawData(endpoint) : await this.fetchV1RawData(endpoint);
    return HomeWizardMapper.toSnapshot({ config: this.config, rawData, online: true, source: 'http' });
  }

  public async execute(commandArg: IHomeWizardCommandRequest): 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;
    }
    if (commandArg.action === 'request_token') {
      return { token: await this.requestToken(commandArg.tokenName) };
    }
    if (!this.hasEndpoint()) {
      throw new HomeWizardApiConnectionError('HomeWizard commands require config.host/config.url, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }

    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new HomeWizardApiConnectionError('HomeWizard local API command requires config.host or config.url.');
    }
    const apiVersion = this.effectiveApiVersion(endpoint);
    if (apiVersion === 'v2' && !this.accessToken()) {
      throw new HomeWizardApiAuthorizationError('HomeWizard API v2 commands require config.token or config.authorizationToken.');
    }

    let result: unknown;
    switch (commandArg.action) {
      case 'set_state':
        result = await this.setState(endpoint, apiVersion, commandArg);
        break;
      case 'set_system':
        result = await this.setSystem(endpoint, apiVersion, commandArg);
        break;
      case 'set_battery_mode':
        result = await this.setBatteryMode(endpoint, apiVersion, commandArg);
        break;
      case 'identify':
        result = await this.identify(endpoint, apiVersion);
        break;
      case 'reboot':
        result = await this.reboot(endpoint, apiVersion);
        break;
      case 'telegram':
        result = await this.telegram(endpoint, apiVersion);
        break;
      default:
        throw new HomeWizardUnsupportedCommandError(`Unsupported HomeWizard command: ${commandArg.action}`);
    }
    this.currentSnapshot = undefined;
    return result;
  }

  public async requestToken(nameArg?: string): Promise<string | undefined> {
    const endpoint = this.endpoint('v2');
    if (!endpoint) {
      throw new HomeWizardApiConnectionError('HomeWizard token requests require config.host or config.url.');
    }
    const response = await this.requestJson<Record<string, unknown>>(endpoint, '/api/user', {
      method: 'POST',
      body: { name: `local/${(nameArg || 'smarthome-exchange').slice(0, 40)}` },
      apiVersion: 'v2',
      allowMissingToken: true,
      allowForbidden: true,
    });
    return typeof response?.token === 'string' && response.token ? response.token : undefined;
  }

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

  private async fetchV1RawData(endpointArg: IHomeWizardEndpoint): Promise<IHomeWizardRawData> {
    const rawData: IHomeWizardRawData = { errors: {}, fetchedAt: new Date().toISOString() };
    rawData.device = await this.requestJson(endpointArg, '/api', { apiVersion: 'v1' });
    rawData.measurement = await this.requestJson(endpointArg, '/api/v1/data', { apiVersion: 'v1' });
    await this.optionalResource(rawData, 'system', endpointArg, '/api/v1/system', 'v1');
    await this.optionalResource(rawData, 'state', endpointArg, '/api/v1/state', 'v1');
    if (!Object.keys(rawData.errors || {}).length) {
      delete rawData.errors;
    }
    return rawData;
  }

  private async fetchV2RawData(endpointArg: IHomeWizardEndpoint): Promise<IHomeWizardRawData> {
    const rawData: IHomeWizardRawData = { errors: {}, fetchedAt: new Date().toISOString() };
    rawData.device = await this.requestJson(endpointArg, '/api', { apiVersion: 'v2' });
    rawData.measurement = await this.requestJson(endpointArg, '/api/measurement', { apiVersion: 'v2' });
    await this.optionalResource(rawData, 'system', endpointArg, '/api/system', 'v2');
    await this.optionalResource(rawData, 'batteries', endpointArg, '/api/batteries', 'v2');
    if (!Object.keys(rawData.errors || {}).length) {
      delete rawData.errors;
    }
    return rawData;
  }

  private async optionalResource(rawDataArg: IHomeWizardRawData, keyArg: 'system' | 'state' | 'batteries', endpointArg: IHomeWizardEndpoint, pathArg: string, apiVersionArg: THomeWizardApiVersion): Promise<void> {
    try {
      rawDataArg[keyArg] = await this.requestJson(endpointArg, pathArg, { apiVersion: apiVersionArg });
    } catch (errorArg) {
      rawDataArg.errors = rawDataArg.errors || {};
      rawDataArg.errors[keyArg] = this.errorMessage(errorArg);
    }
  }

  private async setState(endpointArg: IHomeWizardEndpoint, apiVersionArg: THomeWizardApiVersion, commandArg: IHomeWizardCommandRequest): Promise<unknown> {
    if (apiVersionArg !== 'v1') {
      throw new HomeWizardUnsupportedCommandError('HomeWizard state control is represented by the v1 Energy Socket /api/v1/state endpoint.');
    }
    const body = cleanBody({ power_on: commandArg.powerOn, switch_lock: commandArg.switchLock, brightness: commandArg.brightness === undefined ? undefined : Math.round(commandArg.brightness) });
    return this.requestJson(endpointArg, '/api/v1/state', { method: 'PUT', body, apiVersion: 'v1' });
  }

  private async setSystem(endpointArg: IHomeWizardEndpoint, apiVersionArg: THomeWizardApiVersion, commandArg: IHomeWizardCommandRequest): Promise<unknown> {
    if (apiVersionArg === 'v1') {
      const results: unknown[] = [];
      if (commandArg.cloudEnabled !== undefined) {
        results.push(await this.requestJson(endpointArg, '/api/v1/system', { method: 'PUT', body: { cloud_enabled: commandArg.cloudEnabled }, apiVersion: 'v1' }));
      }
      if (commandArg.statusLedBrightnessPct !== undefined) {
        results.push(await this.requestJson(endpointArg, '/api/v1/state', { method: 'PUT', body: { brightness: Math.round(commandArg.statusLedBrightnessPct * 2.55) }, apiVersion: 'v1' }));
      }
      if (commandArg.apiV1Enabled !== undefined) {
        throw new HomeWizardUnsupportedCommandError('api_v1_enabled is not supported by HomeWizard API v1.');
      }
      return results.length === 1 ? results[0] : results;
    }
    return this.requestJson(endpointArg, '/api/system', {
      method: 'PUT',
      body: cleanBody({ cloud_enabled: commandArg.cloudEnabled, status_led_brightness_pct: commandArg.statusLedBrightnessPct, api_v1_enabled: commandArg.apiV1Enabled }),
      apiVersion: 'v2',
    });
  }

  private async setBatteryMode(endpointArg: IHomeWizardEndpoint, apiVersionArg: THomeWizardApiVersion, commandArg: IHomeWizardCommandRequest): Promise<unknown> {
    if (apiVersionArg !== 'v2') {
      throw new HomeWizardUnsupportedCommandError('HomeWizard battery group mode control requires API v2 /api/batteries.');
    }
    const mode = commandArg.batteryMode;
    if (!mode) {
      throw new HomeWizardUnsupportedCommandError('HomeWizard battery mode commands require a mode/option.');
    }
    return this.requestJson(endpointArg, '/api/batteries', { method: 'PUT', body: batteryModeBody(mode), apiVersion: 'v2' });
  }

  private async identify(endpointArg: IHomeWizardEndpoint, apiVersionArg: THomeWizardApiVersion): Promise<unknown> {
    return apiVersionArg === 'v2'
      ? this.requestJson(endpointArg, '/api/system/identify', { method: 'PUT', apiVersion: 'v2' })
      : this.requestJson(endpointArg, '/api/v1/identify', { method: 'PUT', apiVersion: 'v1' });
  }

  private async reboot(endpointArg: IHomeWizardEndpoint, apiVersionArg: THomeWizardApiVersion): Promise<unknown> {
    if (apiVersionArg !== 'v2') {
      throw new HomeWizardUnsupportedCommandError('HomeWizard reboot requires API v2 /api/system/reboot.');
    }
    return this.requestJson(endpointArg, '/api/system/reboot', { method: 'PUT', apiVersion: 'v2' });
  }

  private async telegram(endpointArg: IHomeWizardEndpoint, apiVersionArg: THomeWizardApiVersion): Promise<string> {
    const response = apiVersionArg === 'v2'
      ? await this.requestText(endpointArg, '/api/telegram', { apiVersion: 'v2' })
      : await this.requestText(endpointArg, '/api/v1/telegram', { apiVersion: 'v1' });
    return response.text;
  }

  private async snapshotFromClient(): Promise<IHomeWizardSnapshot> {
    const client = this.config.client;
    if (!client) {
      throw new HomeWizardApiConnectionError('No HomeWizard client configured.');
    }
    if (client.getSnapshot) {
      const result = await client.getSnapshot();
      if (this.isSnapshot(result)) {
        return HomeWizardMapper.toSnapshot({ config: { ...this.config, snapshot: result }, source: 'client', online: result.online });
      }
      return HomeWizardMapper.toSnapshot({ config: this.config, rawData: result, online: true, source: 'client' });
    }
    if (client.getRawData) {
      return HomeWizardMapper.toSnapshot({ config: this.config, rawData: await client.getRawData(), online: true, source: 'client' });
    }
    throw new HomeWizardApiConnectionError('HomeWizard client must expose getSnapshot() or getRawData().');
  }

  private async requestJson<TValue = Record<string, unknown>>(endpointArg: IHomeWizardEndpoint, pathArg: string, optionsArg: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: unknown; apiVersion: THomeWizardApiVersion; allowMissingToken?: boolean; allowForbidden?: boolean } = { apiVersion: 'v1' }): Promise<TValue> {
    const response = await this.requestText(endpointArg, pathArg, optionsArg);
    if (response.status === 204 || !response.text) {
      return {} as TValue;
    }
    return JSON.parse(response.text) as TValue;
  }

  private async requestText(endpointArg: IHomeWizardEndpoint, pathArg: string, optionsArg: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: unknown; apiVersion: THomeWizardApiVersion; allowMissingToken?: boolean; allowForbidden?: boolean }): Promise<IHomeWizardHttpResponse> {
    const token = this.accessToken();
    if (optionsArg.apiVersion === 'v2' && !token && !optionsArg.allowMissingToken) {
      throw new HomeWizardApiAuthorizationError('HomeWizard API v2 request requires config.token or config.authorizationToken.');
    }
    const headers: Record<string, string> = { 'Content-Type': 'application/json' };
    if (optionsArg.apiVersion === 'v2') {
      headers['X-Api-Version'] = '2';
    }
    if (token) {
      headers.Authorization = `Bearer ${token}`;
    }
    const response = await this.httpRequest(endpointArg, pathArg, {
      method: optionsArg.method || 'GET',
      headers,
      body: optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body),
    });

    if (response.status === 401) {
      throw new HomeWizardApiAuthorizationError('HomeWizard API authorization failed.');
    }
    if (response.status === 403) {
      if (optionsArg.allowForbidden) {
        return response;
      }
      throw new HomeWizardApiDisabledError('HomeWizard local API is disabled or user creation is not enabled.');
    }
    if (response.status === 404) {
      throw new HomeWizardUnsupportedCommandError(`HomeWizard API endpoint is not available: ${pathArg}`);
    }
    if (response.status < 200 || response.status >= 300) {
      throw new HomeWizardApiError(`HomeWizard API request failed with HTTP ${response.status}: ${response.text}`);
    }
    return response;
  }

  private async httpRequest(endpointArg: IHomeWizardEndpoint, pathArg: string, optionsArg: { method: string; headers: Record<string, string>; body?: string }): Promise<IHomeWizardHttpResponse> {
    const transport = endpointArg.protocol === 'https' ? await import('node:https') : await import('node:http');
    const request = (transport as { request: (...args: unknown[]) => { on(eventArg: string, handlerArg: (valueArg: Error) => void): void; write(dataArg: string): void; end(): void; destroy(errorArg?: Error): void } }).request;
    const timeoutMs = this.config.timeoutMs || homeWizardDefaultTimeoutMs;

    return await new Promise<IHomeWizardHttpResponse>((resolve, reject) => {
      const requestOptions: Record<string, unknown> = {
        method: optionsArg.method,
        hostname: endpointArg.host,
        port: endpointArg.port,
        path: pathArg,
        headers: optionsArg.headers,
      };
      if (endpointArg.protocol === 'https') {
        requestOptions.rejectUnauthorized = this.config.verifyTls === true;
        if (this.config.identifier) {
          requestOptions.servername = this.config.identifier;
        }
      }
      const req = request(requestOptions, (res: { statusCode?: number; on(eventArg: string, handlerArg: (chunkArg?: Buffer | string) => void): void }) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunkArg?: Buffer | string) => {
          if (chunkArg !== undefined) {
            chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(String(chunkArg)));
          }
        });
        res.on('end', () => {
          clearTimeout(timer);
          resolve({ status: res.statusCode || 0, text: Buffer.concat(chunks).toString('utf8') });
        });
      });
      const timer = setTimeout(() => {
        req.destroy(new HomeWizardApiConnectionError(`HomeWizard API request timed out after ${timeoutMs}ms.`));
      }, timeoutMs);
      req.on('error', (errorArg: Error) => {
        clearTimeout(timer);
        reject(errorArg instanceof HomeWizardApiError ? errorArg : new HomeWizardApiConnectionError(this.errorMessage(errorArg)));
      });
      if (optionsArg.body !== undefined) {
        req.write(optionsArg.body);
      }
      req.end();
    });
  }

  private endpoint(apiVersionArg?: THomeWizardApiVersion): IHomeWizardEndpoint | undefined {
    const value = this.config.url || this.config.host;
    if (!value) {
      return undefined;
    }
    const version = apiVersionArg || this.config.apiVersion;
    const defaultProtocol = this.config.protocol || (version === 'v2' ? 'https' : 'http');
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `${defaultProtocol}://${value}`);
      const protocol = url.protocol === 'https:' ? 'https' : 'http';
      return {
        protocol,
        host: url.hostname,
        port: this.config.port || (url.port ? Number(url.port) : protocol === 'https' ? homeWizardDefaultHttpsPort : homeWizardDefaultHttpPort),
      };
    } catch {
      const protocol = defaultProtocol;
      return { protocol, host: value, port: this.config.port || (protocol === 'https' ? homeWizardDefaultHttpsPort : homeWizardDefaultHttpPort) };
    }
  }

  private effectiveApiVersion(endpointArg: IHomeWizardEndpoint): THomeWizardApiVersion {
    if (this.config.apiVersion && this.config.apiVersion !== 'auto') {
      return this.config.apiVersion;
    }
    if (this.accessToken() || endpointArg.protocol === 'https') {
      return 'v2';
    }
    return 'v1';
  }

  private accessToken(): string | undefined {
    return stringValue(this.config.token) || stringValue(this.config.authorizationToken);
  }

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

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

  private isSnapshot(valueArg: unknown): valueArg is IHomeWizardSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'measurement' in valueArg && 'capabilities' in valueArg);
  }

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

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

function cleanBody(valueArg: Record<string, unknown>): Record<string, unknown> {
  const body = Object.fromEntries(Object.entries(valueArg).filter(([, value]) => value !== undefined));
  if (!Object.keys(body).length) {
    throw new HomeWizardUnsupportedCommandError('HomeWizard command has no writable values.');
  }
  return body;
}

function batteryModeBody(modeArg: string): Record<string, unknown> {
  switch (modeArg) {
    case 'zero':
      return { mode: 'zero', permissions: ['charge_allowed', 'discharge_allowed'] };
    case 'zero_charge_only':
      return { mode: 'zero', permissions: ['charge_allowed'] };
    case 'zero_discharge_only':
      return { mode: 'zero', permissions: ['discharge_allowed'] };
    case 'standby':
      return { mode: 'standby', permissions: [] };
    case 'to_full':
      return { mode: 'to_full' };
    default:
      return { mode: modeArg };
  }
}

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