import * as http from 'node:http';
import * as https from 'node:https';
import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot, type TSimpleLocalRawData } from '../../core/index.js';
import { LametricMapper, type ILametricCommandRequest } from './lametric.mapper.js';
import type { ILametricConfig, ILametricDeviceData, ILametricSnapshot, TLametricProtocol } from './lametric.types.js';
import { lametricDefaultHttpPort, lametricDefaultHttpsPort, lametricDefaultTimeoutMs, lametricProfile } from './lametric.types.js';

export class LametricApiError extends Error {}
export class LametricApiConnectionError extends LametricApiError {}
export class LametricApiAuthorizationError extends LametricApiError {}
export class LametricUnsupportedCommandError extends LametricApiError {}

interface ILametricEndpoint {
  protocol: TLametricProtocol;
  host: string;
  port: number;
}

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

export class LametricClient extends SimpleLocalClient<ILametricConfig> {
  private cachedSnapshot?: ILametricSnapshot;
  private readonly lametricConfig: ILametricConfig;

  constructor(configArg: ILametricConfig) {
    super(lametricProfile, configArg);
    this.lametricConfig = configArg;
  }

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

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

    if (this.lametricConfig.client) {
      try {
        this.cachedSnapshot = await this.snapshotFromInjectedClient();
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(this.errorMessage(errorArg));
      }
      return LametricMapper.clone(this.cachedSnapshot);
    }

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

    if (this.lametricConfig.rawData !== undefined || this.lametricConfig.rawText !== undefined || this.lametricConfig.entities) {
      this.cachedSnapshot = LametricMapper.toSnapshot({ config: this.lametricConfig, rawData: this.lametricConfig.rawData ?? this.lametricConfig.rawText, online: this.lametricConfig.online ?? true, source: 'manual' });
      return LametricMapper.clone(this.cachedSnapshot);
    }

    if (this.lametricConfig.snapshot) {
      this.cachedSnapshot = LametricMapper.toSnapshot({ config: this.lametricConfig, source: 'snapshot', online: this.lametricConfig.snapshot.online });
      return LametricMapper.clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = this.createOfflineSnapshot('LaMetric snapshots require config.host/config.url with apiKey, an injected client, snapshot, entities, or rawData.');
    return LametricMapper.clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = 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> {
    try {
      if (this.lametricConfig.commandExecutor) {
        return normalizeServiceResult(await this.lametricConfig.commandExecutor.execute(requestArg));
      }
      if (this.lametricConfig.client?.execute) {
        return normalizeServiceResult(await this.lametricConfig.client.execute(requestArg));
      }
      if (!this.hasEndpoint()) {
        return { success: false, error: 'LaMetric commands require config.host/config.url with apiKey, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.' };
      }
      const command = LametricMapper.commandForService(await this.getSnapshot(), requestArg);
      if (!command) {
        return { success: false, error: `Unsupported LaMetric service or target: ${requestArg.domain}.${requestArg.service}` };
      }
      const data = await this.executeCommand(command);
      this.cachedSnapshot = undefined;
      return { success: true, data: data ?? command };
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

  public async fetchSnapshot(): Promise<ILametricSnapshot> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new LametricApiConnectionError('LaMetric local API snapshot requires config.host or config.url.');
    }
    const rawData = await this.requestJson<ILametricDeviceData>(endpoint, '/api/v2/device');
    return LametricMapper.toSnapshot({ config: this.lametricConfig, rawData, online: true, source: 'http' });
  }

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

  private async executeCommand(commandArg: ILametricCommandRequest): Promise<unknown> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new LametricApiConnectionError('LaMetric command requires config.host or config.url.');
    }
    switch (commandArg.action) {
      case 'app_next':
        return this.requestJson(endpoint, '/api/v2/device/apps/next', { method: 'PUT' });
      case 'app_previous':
        return this.requestJson(endpoint, '/api/v2/device/apps/prev', { method: 'PUT' });
      case 'dismiss_current':
        return this.dismissCurrent(endpoint);
      case 'dismiss_all':
        return this.dismissAll(endpoint);
      case 'message':
        return this.sendNotification(endpoint, this.notificationBody(commandArg, 'message'));
      case 'chart':
        return this.sendNotification(endpoint, this.notificationBody(commandArg, 'chart'));
      case 'set_brightness':
        return this.putRequiredNumber(endpoint, '/api/v2/device/display', 'brightness', commandArg.value);
      case 'set_brightness_mode':
        return this.putRequiredString(endpoint, '/api/v2/device/display', 'brightness_mode', commandArg.value);
      case 'set_volume':
        return this.putRequiredNumber(endpoint, '/api/v2/device/audio', 'volume', commandArg.value);
      case 'set_bluetooth':
        return this.putRequiredBoolean(endpoint, '/api/v2/device/bluetooth', 'active', commandArg.value);
      default:
        throw new LametricUnsupportedCommandError(`Unsupported LaMetric command: ${commandArg.action}`);
    }
  }

  private async dismissCurrent(endpointArg: ILametricEndpoint): Promise<unknown> {
    const current = await this.requestJson<Record<string, unknown> | undefined>(endpointArg, '/api/v2/device/notifications/current', { allowNotFound: true });
    const id = notificationId(current);
    return id === undefined ? { dismissed: false } : this.requestJson(endpointArg, `/api/v2/device/notifications/${encodeURIComponent(String(id))}`, { method: 'DELETE' });
  }

  private async dismissAll(endpointArg: ILametricEndpoint): Promise<unknown[]> {
    const queue = await this.requestJson<unknown[]>(endpointArg, '/api/v2/device/notifications');
    const results: unknown[] = [];
    for (const notification of [...queue].reverse()) {
      const id = notificationId(notification);
      if (id !== undefined) {
        results.push(await this.requestJson(endpointArg, `/api/v2/device/notifications/${encodeURIComponent(String(id))}`, { method: 'DELETE' }));
      }
    }
    return results;
  }

  private async sendNotification(endpointArg: ILametricEndpoint, bodyArg: Record<string, unknown>): Promise<unknown> {
    return this.requestJson(endpointArg, '/api/v2/device/notifications', { method: 'POST', body: bodyArg });
  }

  private async putRequiredNumber(endpointArg: ILametricEndpoint, pathArg: string, keyArg: string, valueArg: unknown): Promise<unknown> {
    const value = numberValue(valueArg);
    if (value === undefined) {
      throw new LametricUnsupportedCommandError(`LaMetric ${keyArg} command requires a numeric value.`);
    }
    return this.requestJson(endpointArg, pathArg, { method: 'PUT', body: { [keyArg]: Math.round(value) } });
  }

  private async putRequiredString(endpointArg: ILametricEndpoint, pathArg: string, keyArg: string, valueArg: unknown): Promise<unknown> {
    const value = stringValue(valueArg);
    if (!value) {
      throw new LametricUnsupportedCommandError(`LaMetric ${keyArg} command requires a string value.`);
    }
    return this.requestJson(endpointArg, pathArg, { method: 'PUT', body: { [keyArg]: value } });
  }

  private async putRequiredBoolean(endpointArg: ILametricEndpoint, pathArg: string, keyArg: string, valueArg: unknown): Promise<unknown> {
    const value = booleanValue(valueArg);
    if (value === undefined) {
      throw new LametricUnsupportedCommandError(`LaMetric ${keyArg} command requires a boolean value.`);
    }
    return this.requestJson(endpointArg, pathArg, { method: 'PUT', body: { [keyArg]: value } });
  }

  private notificationBody(commandArg: ILametricCommandRequest, typeArg: 'message' | 'chart'): Record<string, unknown> {
    const data = commandArg.data || {};
    const frames = typeArg === 'chart'
      ? [{ chartData: arrayOfNumbers(data.data ?? data.chartData) }]
      : [cleanBody({ text: stringValue(data.message ?? data.text), icon: iconValue(data.icon) })];
    if (typeArg === 'message' && !stringValue(data.message ?? data.text)) {
      throw new LametricUnsupportedCommandError('LaMetric message notification requires data.message.');
    }
    if (typeArg === 'chart' && !arrayOfNumbers(data.data ?? data.chartData).length) {
      throw new LametricUnsupportedCommandError('LaMetric chart notification requires numeric data points.');
    }

    return cleanBody({
      priority: stringValue(data.priority) || 'info',
      icon_type: stringValue(data.icon_type ?? data.iconType) || 'none',
      lifetime: numberValue(data.lifetime ?? data.lifeTime),
      model: cleanBody({
        frames,
        cycles: numberValue(data.cycles) ?? 1,
        sound: soundBody(data.sound, data.soundCategory, data.soundRepeat),
      }),
    });
  }

  private async snapshotFromInjectedClient(): Promise<ILametricSnapshot> {
    const client = this.lametricConfig.client;
    if (!client) {
      throw new LametricApiConnectionError('No LaMetric client configured.');
    }
    if (client.getSnapshot) {
      const result = await client.getSnapshot();
      if (this.isSimpleSnapshot(result)) {
        return LametricMapper.toSnapshot({ config: { ...this.lametricConfig, snapshot: result }, source: 'client', online: result.online });
      }
      return LametricMapper.toSnapshot({ config: this.lametricConfig, rawData: result, online: true, source: 'client' });
    }
    if (client.getState) {
      return LametricMapper.toSnapshot({ config: this.lametricConfig, rawData: await client.getState(), online: true, source: 'client' });
    }
    if (client.getRawData) {
      return LametricMapper.toSnapshot({ config: this.lametricConfig, rawData: await client.getRawData(), online: true, source: 'client' });
    }
    throw new LametricApiConnectionError('LaMetric client must expose getSnapshot(), getState(), or getRawData().');
  }

  private async requestJson<TValue = Record<string, unknown>>(endpointArg: ILametricEndpoint, pathArg: string, optionsArg: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: unknown; allowNotFound?: boolean } = {}): Promise<TValue> {
    const response = await this.requestText(endpointArg, pathArg, { method: optionsArg.method || 'GET', body: optionsArg.body });
    if (response.status === 401 || response.status === 403) {
      throw new LametricApiAuthorizationError('LaMetric local API authentication failed.');
    }
    if (response.status === 404 && optionsArg.allowNotFound) {
      return undefined as TValue;
    }
    if (response.status < 200 || response.status >= 300) {
      throw new LametricApiError(`LaMetric API request failed with HTTP ${response.status}: ${response.text}`);
    }
    if (!response.text.trim()) {
      return {} as TValue;
    }
    return JSON.parse(response.text) as TValue;
  }

  private async requestText(endpointArg: ILametricEndpoint, pathArg: string, optionsArg: { method: string; body?: unknown }): Promise<ILametricHttpResponse> {
    const apiKey = this.apiKey();
    if (!apiKey) {
      throw new LametricApiAuthorizationError('LaMetric local API requests require config.apiKey, config.token, or config.password.');
    }
    const body = optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body);
    const headers: Record<string, string> = {
      Accept: 'application/json',
      Authorization: `Basic ${Buffer.from(`${this.lametricConfig.username || 'dev'}:${apiKey}`).toString('base64')}`,
    };
    if (body !== undefined) {
      headers['Content-Type'] = 'application/json';
      headers['Content-Length'] = String(Buffer.byteLength(body));
    }

    return await new Promise<ILametricHttpResponse>((resolve, reject) => {
      const requestOptions: http.RequestOptions & { rejectUnauthorized?: boolean } = {
        method: optionsArg.method,
        hostname: endpointArg.host,
        port: endpointArg.port,
        path: pathArg,
        headers,
      };
      if (endpointArg.protocol === 'https') {
        requestOptions.rejectUnauthorized = this.lametricConfig.verifyTls === true;
      }
      const transport = endpointArg.protocol === 'https' ? https : http;
      const request = transport.request(requestOptions, (responseArg) => {
        const chunks: Buffer[] = [];
        responseArg.on('data', (chunkArg: Buffer | string) => chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(String(chunkArg))));
        responseArg.on('end', () => {
          clearTimeout(timer);
          resolve({ status: responseArg.statusCode || 0, text: Buffer.concat(chunks).toString('utf8') });
        });
      });
      const timer = setTimeout(() => request.destroy(new LametricApiConnectionError(`LaMetric API request to ${endpointArg.host}:${endpointArg.port} timed out after ${this.timeoutMs()}ms.`)), this.timeoutMs());
      request.on('error', (errorArg) => {
        clearTimeout(timer);
        reject(errorArg instanceof LametricApiError ? errorArg : new LametricApiConnectionError(this.errorMessage(errorArg)));
      });
      if (body !== undefined) {
        request.write(body);
      }
      request.end();
    });
  }

  private endpoint(): ILametricEndpoint | undefined {
    const value = this.lametricConfig.url || this.lametricConfig.host;
    if (!value) {
      return undefined;
    }
    const defaultProtocol: TLametricProtocol = this.lametricConfig.protocol || (this.lametricConfig.useTls === false ? 'http' : 'https');
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `${defaultProtocol}://${value}`);
      const protocol: TLametricProtocol = url.protocol === 'http:' ? 'http' : 'https';
      return { protocol, host: url.hostname, port: this.lametricConfig.port || (url.port ? Number(url.port) : protocol === 'http' ? lametricDefaultHttpPort : lametricDefaultHttpsPort) };
    } catch {
      return { protocol: defaultProtocol, host: value, port: this.lametricConfig.port || (defaultProtocol === 'http' ? lametricDefaultHttpPort : lametricDefaultHttpsPort) };
    }
  }

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

  private apiKey(): string | undefined {
    return stringValue(this.lametricConfig.apiKey) || stringValue(this.lametricConfig.token) || stringValue(this.lametricConfig.password);
  }

  private timeoutMs(): number {
    return this.lametricConfig.timeoutMs || lametricDefaultTimeoutMs;
  }

  private createOfflineSnapshot(errorArg: string): ILametricSnapshot {
    return LametricMapper.toSnapshot({ config: this.lametricConfig, rawData: this.lametricConfig.rawData, online: false, source: 'runtime', error: errorArg });
  }

  private isSimpleSnapshot(valueArg: unknown): valueArg is ISimpleLocalSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg);
  }

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

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

function notificationId(valueArg: unknown): string | number | undefined {
  const value = valueArg && typeof valueArg === 'object' ? valueArg as Record<string, unknown> : undefined;
  return stringValue(value?.id) || stringValue(value?.notification_id) || numberValue(value?.id) || numberValue(value?.notification_id);
}

function soundBody(soundArg: unknown, categoryArg: unknown, repeatArg: unknown): Record<string, unknown> | undefined {
  const sound = stringValue(soundArg);
  if (!sound) {
    return undefined;
  }
  return cleanBody({
    category: stringValue(categoryArg) || (sound.startsWith('alarm') ? 'alarms' : 'notifications'),
    id: sound,
    repeat: numberValue(repeatArg) ?? 1,
  });
}

function iconValue(valueArg: unknown): string | number | undefined {
  const value = stringValue(valueArg);
  if (!value) {
    return undefined;
  }
  return /^\d+$/.test(value) ? Number(value) : value;
}

function arrayOfNumbers(valueArg: unknown): number[] {
  const values = Array.isArray(valueArg) ? valueArg : typeof valueArg === 'string' ? valueArg.split(',') : [];
  return values.map((value) => numberValue(value)).filter((value): value is number => value !== undefined).map((value) => Math.round(value));
}

function cleanBody(valueArg: Record<string, unknown>): Record<string, unknown> {
  return Object.fromEntries(Object.entries(valueArg).filter(([, value]) => value !== undefined));
}

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

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

function booleanValue(valueArg: unknown): boolean | undefined {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'string') {
    if (['true', 'on', '1'].includes(valueArg.toLowerCase())) {
      return true;
    }
    if (['false', 'off', '0'].includes(valueArg.toLowerCase())) {
      return false;
    }
  }
  return undefined;
}
