import type { IncomingHttpHeaders } from 'node:http';
import * as plugins from '../../plugins.js';
import { FreeboxMapper } from './freebox.mapper.js';
import type { IFreeboxAuthorizationProgress, IFreeboxAuthorizationRequest, IFreeboxClientLike, IFreeboxCommandRequest, IFreeboxConfig, IFreeboxRawData, IFreeboxRefreshResult, IFreeboxSession, IFreeboxSnapshot, TFreeboxProtocol } from './freebox.types.js';
import { freeboxAppDescription, freeboxDefaultApiBaseUrl, freeboxDefaultApiVersion, freeboxDefaultHost, freeboxDefaultPort, freeboxDefaultTimeoutMs } from './freebox.types.js';

export class FreeboxApiError extends Error {}
export class FreeboxApiConnectionError extends FreeboxApiError {}
export class FreeboxApiAuthorizationError extends FreeboxApiError {}
export class FreeboxApiPermissionError extends FreeboxApiAuthorizationError {}
export class FreeboxApiInvalidResponseError extends FreeboxApiError {}

interface IFreeboxEndpoint {
  protocol: TFreeboxProtocol;
  host: string;
  port: number;
}

interface IFreeboxHttpResponse {
  status: number;
  text: string;
  headers: IncomingHttpHeaders;
}

interface IRequestOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  body?: unknown;
  auth?: boolean;
  apiVersion?: string;
  rawPath?: boolean;
  retryAuth?: boolean;
}

export class FreeboxClient {
  private currentSnapshot?: IFreeboxSnapshot;
  private sessionToken?: string;
  private permissions?: Record<string, boolean>;

  constructor(private readonly config: IFreeboxConfig) {
    this.sessionToken = config.sessionToken;
  }

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

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

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

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

    if (this.endpoint()) {
      if (!this.accessToken() && !this.sessionToken) {
        this.currentSnapshot = FreeboxMapper.offlineSnapshot(this.config, 'Freebox local API snapshots require config.appToken/token or config.sessionToken. Use app authorization on the Freebox before polling authenticated endpoints.');
        return FreeboxMapper.clone(this.currentSnapshot);
      }
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = FreeboxMapper.offlineSnapshot(this.config, this.errorMessage(errorArg));
      }
      return FreeboxMapper.clone(this.currentSnapshot);
    }

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

  public async refresh(): Promise<IFreeboxRefreshResult> {
    try {
      this.currentSnapshot = undefined;
      const liveCapable = Boolean(this.config.client || (this.endpoint() && (this.accessToken() || this.sessionToken)));
      const snapshot = await this.getSnapshot(liveCapable);
      const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
      return { success, snapshot, error: success ? undefined : snapshot.error || 'Freebox refresh requires a reachable authenticated local API endpoint or injected client.', data: { source: snapshot.source } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = FreeboxMapper.offlineSnapshot(this.config, error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: FreeboxMapper.clone(snapshot), error };
    }
  }

  public async fetchSnapshot(): Promise<IFreeboxSnapshot> {
    if (!this.endpoint()) {
      throw new FreeboxApiConnectionError('Freebox local API snapshot requires config.host or config.url.');
    }
    if (!this.accessToken() && !this.sessionToken) {
      throw new FreeboxApiAuthorizationError('Freebox local API snapshot requires config.appToken/token or config.sessionToken.');
    }

    const rawData: Partial<IFreeboxRawData> = { errors: {}, fetchedAt: new Date().toISOString() };
    rawData.apiVersionInfo = await this.discoverApiVersion().catch((errorArg) => this.captureError(rawData, 'apiVersionInfo', errorArg));
    rawData.systemConfig = await this.optionalApiResource(rawData, 'systemConfig', 'system/');
    rawData.connectionStatus = await this.optionalApiResource(rawData, 'connectionStatus', 'connection/');
    rawData.callLog = await this.optionalApiResource(rawData, 'callLog', 'call/log/');
    rawData.disks = await this.optionalApiResource(rawData, 'disks', 'storage/disk/');
    rawData.raids = await this.optionalApiResource(rawData, 'raids', 'storage/raid/');
    rawData.wifiConfig = await this.optionalApiResource(rawData, 'wifiConfig', 'wifi/config/', this.wifiApiVersion());
    rawData.homeNodes = await this.optionalApiResource(rawData, 'homeNodes', 'home/nodes');

    const lanInterfaces = await this.optionalApiResource<Array<Record<string, unknown>>>(rawData, 'lanInterfaces', 'lan/browser/interfaces/');
    rawData.lanInterfaces = lanInterfaces;
    rawData.lanHosts = [];
    for (const lanInterface of Array.isArray(lanInterfaces) ? lanInterfaces : []) {
      const name = stringValue(lanInterface.name);
      if (!name) {
        continue;
      }
      const hosts = await this.optionalApiResource<Array<Record<string, unknown>>>(rawData, `lanHosts.${name}`, `lan/browser/${encodeURIComponent(name)}/`);
      if (Array.isArray(hosts)) {
        rawData.lanHosts.push(...hosts);
      }
    }

    if (!Object.keys(rawData.errors || {}).length) {
      delete rawData.errors;
    }
    if (!FreeboxMapper.hasUsableRawData(rawData)) {
      const error = Object.values(rawData.errors || {})[0] || 'Freebox local API did not return system, connection, call, storage, LAN, WiFi, or Home data.';
      throw new FreeboxApiInvalidResponseError(error);
    }

    return FreeboxMapper.toSnapshot({ config: this.config, rawData, online: true, source: 'http' });
  }

  public async requestAuthorization(): Promise<IFreeboxAuthorizationRequest> {
    const app = this.appDescription();
    return await this.requestApiJson<IFreeboxAuthorizationRequest>('login/authorize/', {
      method: 'POST',
      auth: false,
      body: {
        app_id: app.appId,
        app_name: app.appName,
        app_version: app.appVersion,
        device_name: app.deviceName,
      },
    });
  }

  public async trackAuthorization(trackIdArg: number): Promise<IFreeboxAuthorizationProgress> {
    return await this.requestApiJson<IFreeboxAuthorizationProgress>(`login/authorize/${encodeURIComponent(String(trackIdArg))}`, { auth: false });
  }

  public async execute(commandArg: IFreeboxCommandRequest): 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 === 'snapshot') {
      return this.getSnapshot();
    }
    if (!commandArg.endpoint || !commandArg.method) {
      throw new FreeboxApiConnectionError('Freebox command has no local API endpoint. Provide an injected client.execute or commandExecutor for this command.');
    }
    if (!this.endpoint() || (!this.accessToken() && !this.sessionToken)) {
      throw new FreeboxApiConnectionError('Freebox commands require config.host/config.url and appToken/token/sessionToken, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }
    const apiVersion = commandArg.endpoint.startsWith('wifi/') ? this.wifiApiVersion() : this.apiVersion();
    const result = await this.requestApiJson(commandArg.endpoint, { method: commandArg.method, body: commandArg.body, apiVersion });
    this.currentSnapshot = undefined;
    return result ?? commandArg;
  }

  public async destroy(): Promise<void> {
    if (this.sessionToken && !this.config.sessionToken) {
      await this.requestApiJson('login/logout/', { method: 'POST' }).catch(() => undefined);
      this.sessionToken = undefined;
    }
    await this.config.client?.destroy?.();
  }

  private async snapshotFromClient(clientArg: IFreeboxClientLike): Promise<IFreeboxSnapshot> {
    if (clientArg.getSnapshot) {
      const result = await clientArg.getSnapshot();
      if (this.isSnapshot(result)) {
        return FreeboxMapper.toSnapshot({ config: { ...this.config, snapshot: result }, source: 'client', online: result.online });
      }
      return FreeboxMapper.toSnapshot({ config: this.config, rawData: result as Partial<IFreeboxRawData>, online: true, source: 'client' });
    }
    if (clientArg.getRawData) {
      return FreeboxMapper.toSnapshot({ config: this.config, rawData: await clientArg.getRawData(), online: true, source: 'client' });
    }
    throw new FreeboxApiConnectionError('Freebox client must expose getSnapshot() or getRawData().');
  }

  private async optionalApiResource<TValue = unknown>(rawDataArg: Partial<IFreeboxRawData>, keyArg: string, pathArg: string, apiVersionArg = this.apiVersion()): Promise<TValue | undefined> {
    try {
      return await this.requestApiJson<TValue>(pathArg, { apiVersion: apiVersionArg });
    } catch (errorArg) {
      rawDataArg.errors = rawDataArg.errors || {};
      rawDataArg.errors[keyArg] = this.errorMessage(errorArg);
      return undefined;
    }
  }

  private captureError<TValue>(rawDataArg: Partial<IFreeboxRawData>, keyArg: string, errorArg: unknown): TValue | undefined {
    rawDataArg.errors = rawDataArg.errors || {};
    rawDataArg.errors[keyArg] = this.errorMessage(errorArg);
    return undefined;
  }

  private async discoverApiVersion(): Promise<Record<string, unknown>> {
    return await this.requestJson<Record<string, unknown>>('/api_version', { auth: false, rawPath: true });
  }

  private async ensureSession(): Promise<void> {
    if (this.sessionToken) {
      return;
    }
    const appToken = this.accessToken();
    if (!appToken) {
      throw new FreeboxApiAuthorizationError('Freebox authentication requires config.appToken/token or config.sessionToken.');
    }
    const app = this.appDescription();
    const challenge = (await this.requestApiJson<{ challenge?: string }>('login/', { auth: false })).challenge;
    if (!challenge) {
      throw new FreeboxApiAuthorizationError('Freebox login did not return a challenge.');
    }
    const password = plugins.crypto.createHmac('sha1', appToken).update(challenge).digest('hex');
    const session = await this.requestApiJson<IFreeboxSession>('login/session/', {
      method: 'POST',
      auth: false,
      body: { app_id: app.appId, password },
    });
    if (!session.session_token) {
      throw new FreeboxApiAuthorizationError('Freebox login did not return a session token.');
    }
    this.sessionToken = session.session_token;
    this.permissions = session.permissions;
  }

  private async requestApiJson<TValue = unknown>(pathArg: string, optionsArg: IRequestOptions = {}): Promise<TValue> {
    return await this.requestJson<TValue>(this.apiPath(pathArg, optionsArg.apiVersion), { ...optionsArg, rawPath: true });
  }

  private async requestJson<TValue = unknown>(pathArg: string, optionsArg: IRequestOptions = {}): Promise<TValue> {
    if (optionsArg.auth !== false) {
      await this.ensureSession();
    }

    const response = await this.requestText(pathArg, optionsArg);
    const json = this.parseJson(response.text, pathArg);
    if ('success' in json) {
      if (json.success === true) {
        return ('result' in json ? json.result : undefined) as TValue;
      }
      const errorCode = stringValue(json.error_code);
      if ((errorCode === 'auth_required' || errorCode === 'invalid_session') && optionsArg.retryAuth !== false && this.accessToken()) {
        this.sessionToken = undefined;
        await this.ensureSession();
        return await this.requestJson<TValue>(pathArg, { ...optionsArg, retryAuth: false });
      }
      const message = stringValue(json.msg) || stringValue(json.error_code) || `HTTP ${response.status}`;
      if (errorCode === 'insufficient_rights') {
        throw new FreeboxApiPermissionError(`Freebox API permission denied at ${pathArg}: ${message}`);
      }
      throw new FreeboxApiError(`Freebox API request failed at ${pathArg}: ${message}`);
    }
    return json as TValue;
  }

  private async requestText(pathArg: string, optionsArg: IRequestOptions): Promise<IFreeboxHttpResponse> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new FreeboxApiConnectionError('Freebox request requires config.host or config.url.');
    }
    const response = await this.httpRequest(endpoint, pathArg.startsWith('/') ? pathArg : `/${pathArg}`, optionsArg);
    if ((response.status === 401 || response.status === 403) && optionsArg.auth !== false && optionsArg.retryAuth !== false && this.accessToken()) {
      this.sessionToken = undefined;
      await this.ensureSession();
      return await this.requestText(pathArg, { ...optionsArg, retryAuth: false });
    }
    if (response.status < 200 || response.status >= 300) {
      throw new FreeboxApiConnectionError(`Freebox API request failed with HTTP ${response.status}: ${response.text}`);
    }
    return response;
  }

  private async httpRequest(endpointArg: IFreeboxEndpoint, pathArg: string, optionsArg: IRequestOptions): Promise<IFreeboxHttpResponse> {
    const transport = endpointArg.protocol === 'https' ? await import('node:https') : await import('node:http');
    const timeoutMs = this.config.timeoutMs || freeboxDefaultTimeoutMs;
    return await new Promise<IFreeboxHttpResponse>((resolve, reject) => {
      const body = optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body);
      const headers: Record<string, string> = { Accept: 'application/json' };
      if (this.sessionToken && optionsArg.auth !== false) {
        headers['X-Fbx-App-Auth'] = this.sessionToken;
      }
      if (body !== undefined) {
        headers['Content-Type'] = 'application/json';
        headers['Content-Length'] = String(Buffer.byteLength(body));
      }
      const requestOptions: Record<string, unknown> = {
        method: optionsArg.method || 'GET',
        hostname: endpointArg.host,
        port: endpointArg.port,
        path: pathArg,
        headers,
      };
      if (endpointArg.protocol === 'https') {
        requestOptions.rejectUnauthorized = this.config.verifyTls === true;
      }
      const request = transport.request(requestOptions, (res) => {
        const chunks: Buffer[] = [];
        res.on('data', (chunkArg: Buffer | string) => 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'), headers: res.headers });
        });
      });
      const timer = setTimeout(() => request.destroy(new FreeboxApiConnectionError(`Freebox API request timed out after ${timeoutMs}ms.`)), timeoutMs);
      request.on('error', (errorArg: Error) => {
        clearTimeout(timer);
        reject(errorArg instanceof FreeboxApiError ? errorArg : new FreeboxApiConnectionError(this.errorMessage(errorArg)));
      });
      if (body !== undefined) {
        request.write(body);
      }
      request.end();
    });
  }

  private apiPath(pathArg: string, apiVersionArg = this.apiVersion()): string {
    const base = (this.config.apiBaseUrl || freeboxDefaultApiBaseUrl).replace(/\/+$/, '/');
    const version = normalizeApiVersion(apiVersionArg);
    return `${base}${version}/${pathArg.replace(/^\/+/, '')}`;
  }

  private endpoint(): IFreeboxEndpoint | undefined {
    const value = this.config.url || this.config.host || freeboxDefaultHost;
    const defaultProtocol = this.config.protocol || 'https';
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `${defaultProtocol}://${value}`);
      const protocol = url.protocol === 'http:' ? 'http' : 'https';
      return { protocol, host: url.hostname, port: this.config.port || (url.port ? Number(url.port) : protocol === 'https' ? freeboxDefaultPort : 80) };
    } catch {
      return { protocol: defaultProtocol, host: value, port: this.config.port || (defaultProtocol === 'https' ? freeboxDefaultPort : 80) };
    }
  }

  private apiVersion(): string {
    return normalizeApiVersion(this.config.apiVersion || freeboxDefaultApiVersion);
  }

  private wifiApiVersion(): string {
    return normalizeApiVersion(this.config.wifiApiVersion || this.config.apiVersion || freeboxDefaultApiVersion);
  }

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

  private appDescription(): Required<NonNullable<IFreeboxConfig['appDescription']>> {
    return {
      appId: this.config.appDescription?.appId || freeboxAppDescription.appId,
      appName: this.config.appDescription?.appName || freeboxAppDescription.appName,
      appVersion: this.config.appDescription?.appVersion || freeboxAppDescription.appVersion,
      deviceName: this.config.appDescription?.deviceName || 'smarthome.exchange',
    };
  }

  private parseJson(textArg: string, pathArg: string): Record<string, unknown> {
    try {
      const result = textArg ? JSON.parse(textArg) as unknown : {};
      if (!result || typeof result !== 'object' || Array.isArray(result)) {
        throw new Error('JSON root is not an object.');
      }
      return result as Record<string, unknown>;
    } catch (errorArg) {
      throw new FreeboxApiInvalidResponseError(`Freebox API returned non-JSON data at ${pathArg}: ${this.errorMessage(errorArg)}`);
    }
  }

  private isSnapshot(valueArg: unknown): valueArg is IFreeboxSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'router' in valueArg && 'sensors' in valueArg && 'capabilities' in valueArg);
  }

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

function normalizeApiVersion(valueArg: string): string {
  return valueArg.startsWith('v') ? valueArg : `v${valueArg.split('.')[0] || '6'}`;
}

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