import * as http from 'node:http';
import * as https from 'node:https';
import { AirosMapper } from './airos.mapper.js';
import { airosDefaultHttpPort, airosDefaultHttpsPort, airosDefaultTimeoutMs, airosDefaultUsername } from './airos.constants.js';
import type { IAirosCommand, IAirosCommandResult, IAirosConfig, IAirosEvent, IAirosFirmwareStatus, IAirosNativeClient, IAirosRawStatus, IAirosSnapshot, IAirosValueMap } from './airos.types.js';

type TAirosEventHandler = (eventArg: IAirosEvent) => void;

export class AirosApiError extends Error {}
export class AirosApiConnectionError extends AirosApiError {}
export class AirosApiAuthenticationError extends AirosApiError {}
export class AirosApiNotFoundError extends AirosApiError {}
export class AirosApiDataError extends AirosApiError {}

export class AirosClient {
  private snapshot?: IAirosSnapshot;
  private readonly events: IAirosEvent[] = [];
  private readonly eventHandlers = new Set<TAirosEventHandler>();

  constructor(private readonly config: IAirosConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IAirosSnapshot> {
    if (!forceRefreshArg && this.snapshot) {
      return this.cloneSnapshot(this.snapshot);
    }
    if (!forceRefreshArg && this.config.snapshot) {
      this.snapshot = AirosMapper.toSnapshot(this.config, { source: 'snapshot' }, this.events);
      return this.cloneSnapshot(this.snapshot);
    }

    try {
      if (this.config.transport?.readSnapshot) {
        this.snapshot = AirosMapper.mergeSnapshot(AirosMapper.toSnapshot(this.config, {}, this.events), await this.config.transport.readSnapshot());
        return this.cloneSnapshot(this.snapshot);
      }
      if (this.config.nativeClient) {
        this.snapshot = await this.snapshotFromNativeClient(this.config.nativeClient);
        return this.cloneSnapshot(this.snapshot);
      }
      if (this.config.rawStatus) {
        this.snapshot = AirosMapper.toSnapshot(this.config, { source: 'manual', connected: this.config.connected ?? true }, this.events);
        return this.cloneSnapshot(this.snapshot);
      }
      if (this.canUseNativeHttp()) {
        this.snapshot = await this.fetchSnapshot();
        return this.cloneSnapshot(this.snapshot);
      }
      this.snapshot = this.offlineSnapshot(this.unsupportedReadMessage());
      return this.cloneSnapshot(this.snapshot);
    } catch (errorArg) {
      this.snapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      return this.cloneSnapshot(this.snapshot);
    }
  }

  public onEvent(handlerArg: TAirosEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async refresh(): Promise<IAirosCommandResult> {
    this.snapshot = undefined;
    const snapshot = await this.getSnapshot(Boolean(this.config.host || this.config.nativeClient || this.config.transport?.readSnapshot));
    this.emit({ type: 'snapshot_refreshed', data: this.cloneSnapshot(snapshot), timestamp: Date.now() });
    const success = snapshot.connected && !snapshot.error && snapshot.source !== 'runtime';
    return { success, transmitted: false, data: { snapshot: this.cloneSnapshot(snapshot), source: snapshot.source }, error: success ? undefined : snapshot.error };
  }

  public async sendCommand(commandArg: IAirosCommand): Promise<IAirosCommandResult> {
    this.emit({ type: 'command_mapped', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, timestamp: Date.now() });
    try {
      const result = await this.executeCommand(commandArg);
      if (result.success) {
        this.patchSnapshot(commandArg, result);
      }
      this.emit({
        type: result.success ? 'command_succeeded' : 'command_failed',
        command: commandArg,
        deviceId: commandArg.deviceId,
        entityId: commandArg.entityId,
        data: result,
        error: result.error,
        timestamp: Date.now(),
      });
      return result;
    } catch (errorArg) {
      const result: IAirosCommandResult = { success: false, transmitted: false, action: commandArg.action, error: this.errorMessage(errorArg), data: { command: commandArg } };
      this.emit({ type: 'command_failed', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, data: result, error: result.error, timestamp: Date.now() });
      return result;
    }
  }

  public async fetchSnapshot(): Promise<IAirosSnapshot> {
    const client = this.localWebClient();
    await client.login();
    const rawStatus = await client.getRawStatus();
    const derived = AirosMapper.derive(rawStatus, this.config);
    const firmware = (derived.fwMajor || 0) >= 8 ? await client.updateCheck().catch(() => undefined) : undefined;
    return AirosMapper.toSnapshot({ ...this.config, rawStatus, firmwareStatus: firmware }, { source: 'http', connected: true, device: { authenticated: true } }, this.events);
  }

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

  private async snapshotFromNativeClient(clientArg: IAirosNativeClient): Promise<IAirosSnapshot> {
    await clientArg.login?.();
    if (clientArg.getSnapshot) {
      const result = await clientArg.getSnapshot();
      if (this.isSnapshot(result)) {
        return AirosMapper.toSnapshot({ ...this.config, snapshot: result }, { source: 'client', connected: result.connected }, this.events);
      }
      return AirosMapper.mergeSnapshot(AirosMapper.toSnapshot(this.config, { source: 'client', connected: true }, this.events), result);
    }
    const getRawStatus = clientArg.getRawStatus || clientArg.rawStatus || clientArg.status;
    if (!getRawStatus) {
      throw new AirosApiConnectionError('airOS nativeClient must expose getSnapshot(), getRawStatus(), rawStatus(), or status().');
    }
    const rawStatus = await getRawStatus.call(clientArg);
    const updateCheck = clientArg.updateCheck || clientArg.update_check;
    const derived = AirosMapper.derive(rawStatus, this.config);
    const firmware = updateCheck && (derived.fwMajor || 0) >= 8 ? await updateCheck.call(clientArg).catch(() => undefined) : undefined;
    return AirosMapper.toSnapshot({ ...this.config, rawStatus, firmwareStatus: firmware }, { source: 'client', connected: true, device: { authenticated: true } }, this.events);
  }

  private async executeCommand(commandArg: IAirosCommand): Promise<IAirosCommandResult> {
    if (commandArg.action === 'refresh') {
      return this.refresh();
    }
    const executor = this.executor();
    if (executor) {
      return this.commandResult(await executor({ ...commandArg, method: 'executor' }), { ...commandArg, method: 'executor' }, true);
    }
    if (this.config.transport?.execute) {
      return this.commandResult(await this.config.transport.execute({ ...commandArg, method: 'executor' }), { ...commandArg, method: 'executor' }, true);
    }
    if (this.config.nativeClient) {
      return this.executeNativeClientCommand(this.config.nativeClient, commandArg);
    }
    if (this.canUseNativeHttp()) {
      const client = this.localWebClient();
      await client.login();
      switch (commandArg.action) {
        case 'reboot':
          return this.commandResult(await client.reboot(), commandArg, true);
        case 'download_update':
          return this.commandResult(await client.download(), commandArg, true);
        case 'install_update':
          await client.download();
          return this.commandResult(await client.install(), commandArg, true);
        case 'firmware_update_check':
          return this.commandResult(await client.updateCheck(true), commandArg, true);
        default:
          return { success: false, transmitted: false, action: commandArg.action, error: `Unsupported airOS command: ${commandArg.action}`, data: { command: commandArg } };
      }
    }
    return {
      success: false,
      transmitted: false,
      action: commandArg.action,
      error: 'airOS commands require host/password native HTTP, nativeClient, commandExecutor, or transport.execute. Static snapshots and raw status data are read-only.',
      data: { command: commandArg },
    };
  }

  private async executeNativeClientCommand(clientArg: IAirosNativeClient, commandArg: IAirosCommand): Promise<IAirosCommandResult> {
    await clientArg.login?.();
    switch (commandArg.action) {
      case 'reboot':
        if (clientArg.reboot) {
          return this.commandResult(await clientArg.reboot(), { ...commandArg, method: 'native_client' }, true);
        }
        break;
      case 'download_update':
        if (clientArg.download) {
          return this.commandResult(await clientArg.download(), { ...commandArg, method: 'native_client' }, true);
        }
        break;
      case 'install_update':
        if (clientArg.install) {
          await clientArg.download?.();
          return this.commandResult(await clientArg.install(), { ...commandArg, method: 'native_client' }, true);
        }
        break;
      case 'firmware_update_check': {
        const updateCheck = clientArg.updateCheck || clientArg.update_check;
        if (updateCheck) {
          return this.commandResult(await updateCheck.call(clientArg, true), { ...commandArg, method: 'native_client' }, true);
        }
        break;
      }
      default:
        break;
    }
    return { success: false, transmitted: false, action: commandArg.action, error: `airOS nativeClient does not implement ${commandArg.action}.`, data: { command: commandArg } };
  }

  private commandResult(resultArg: unknown, commandArg: IAirosCommand, transmittedDefaultArg: boolean): IAirosCommandResult {
    if (this.isCommandResult(resultArg)) {
      return { transmitted: transmittedDefaultArg, action: commandArg.action, ...resultArg };
    }
    if (resultArg === false) {
      return { success: false, transmitted: transmittedDefaultArg, action: commandArg.action, error: 'airOS command executor reported failure.', data: { command: commandArg, response: resultArg } };
    }
    if (resultArg && typeof resultArg === 'object') {
      const record = resultArg as Record<string, unknown>;
      if (record.ok === false || record.error) {
        return { success: false, transmitted: transmittedDefaultArg, action: commandArg.action, error: String(record.error || 'airOS command returned ok=false.'), data: { command: commandArg, response: resultArg } };
      }
    }
    return { success: true, transmitted: transmittedDefaultArg, action: commandArg.action, data: { command: commandArg, response: resultArg } };
  }

  private patchSnapshot(commandArg: IAirosCommand, resultArg: IAirosCommandResult): void {
    const current = this.snapshot || AirosMapper.toSnapshot(this.config, {}, this.events);
    this.snapshot = AirosMapper.mergeSnapshot(current, {
      metadata: { lastCommand: commandArg.action, lastCommandTransmitted: resultArg.transmitted, lastCommandAt: new Date().toISOString() },
      updatedAt: new Date().toISOString(),
    });
    this.emit({ type: 'state_changed', command: commandArg, deviceId: commandArg.deviceId, entityId: commandArg.entityId, data: this.cloneSnapshot(this.snapshot), timestamp: Date.now() });
  }

  private executor(): ((commandArg: IAirosCommand) => Promise<IAirosCommandResult | unknown>) | undefined {
    if (!this.config.commandExecutor) {
      return undefined;
    }
    if (typeof this.config.commandExecutor === 'function') {
      return this.config.commandExecutor;
    }
    return this.config.commandExecutor.execute.bind(this.config.commandExecutor);
  }

  private canUseNativeHttp(): boolean {
    return this.config.nativeHttpEnabled !== false && Boolean(this.config.host && this.config.password);
  }

  private localWebClient(): AirosLocalWebClient {
    return new AirosLocalWebClient({
      host: this.config.host as string,
      port: this.config.port,
      username: this.config.username || airosDefaultUsername,
      password: this.config.password as string,
      ssl: this.config.ssl ?? this.config.useSsl ?? this.config.advanced_settings?.ssl ?? true,
      verifySsl: this.config.verifySsl ?? this.config.verify_ssl ?? this.config.advanced_settings?.verifySsl ?? this.config.advanced_settings?.verify_ssl ?? false,
      timeoutMs: this.config.timeoutMs || airosDefaultTimeoutMs,
    });
  }

  private offlineSnapshot(errorArg: string): IAirosSnapshot {
    return AirosMapper.toSnapshot(this.config, { connected: false, source: 'runtime', error: errorArg, device: { available: false } }, this.events);
  }

  private unsupportedReadMessage(): string {
    return 'No airOS host/password native HTTP, nativeClient, transport.readSnapshot, snapshot, or rawStatus is configured.';
  }

  private isSnapshot(valueArg: unknown): valueArg is IAirosSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'host' in valueArg && 'wireless' in valueArg && 'services' in valueArg);
  }

  private isCommandResult(valueArg: unknown): valueArg is IAirosCommandResult {
    return Boolean(valueArg && typeof valueArg === 'object' && 'success' in valueArg);
  }

  private emit(eventArg: IAirosEvent): void {
    this.events.push(eventArg);
    for (const handler of this.eventHandlers) {
      handler(eventArg);
    }
  }

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

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

export class AirosLocalWebClient implements IAirosNativeClient {
  private readonly baseUrl: URL;
  private authCookie?: string;
  private csrfId?: string;
  private connected = false;

  constructor(private readonly options: {
    host: string;
    port?: number;
    username?: string;
    password: string;
    ssl?: boolean;
    verifySsl?: boolean;
    timeoutMs?: number;
  }) {
    this.baseUrl = this.resolveBaseUrl(options);
  }

  public async login(): Promise<boolean> {
    try {
      await this.requestJson('POST', '/api/auth', { json: { username: this.options.username || airosDefaultUsername, password: this.options.password }, isLogin: true });
      this.connected = true;
      return true;
    } catch (errorArg) {
      if (errorArg instanceof AirosApiNotFoundError) {
        await this.loginV6();
        return true;
      }
      throw errorArg;
    }
  }

  public async getSnapshot(): Promise<IAirosSnapshot> {
    await this.login();
    const rawStatus = await this.getRawStatus();
    const firmware = (AirosMapper.derive(rawStatus).fwMajor || 0) >= 8 ? await this.updateCheck().catch(() => undefined) : undefined;
    return AirosMapper.toSnapshot({ host: this.options.host, port: this.options.port, username: this.options.username, ssl: this.options.ssl, verifySsl: this.options.verifySsl, rawStatus, firmwareStatus: firmware }, { source: 'http', connected: true, device: { authenticated: true } });
  }

  public async getRawStatus(): Promise<IAirosRawStatus> {
    if (!this.connected) {
      await this.login();
    }
    return this.requestJson('GET', '/status.cgi', { authenticated: true }) as Promise<IAirosRawStatus>;
  }

  public async updateCheck(forceArg = false): Promise<IAirosFirmwareStatus> {
    if (!this.connected) {
      await this.login();
    }
    return this.requestJson('POST', '/api/fw/update-check', { authenticated: true, json: forceArg ? { force: true } : {} }) as Promise<IAirosFirmwareStatus>;
  }

  public async reboot(): Promise<IAirosCommandResult | IAirosValueMap> {
    if (!this.connected) {
      await this.login();
    }
    const result = await this.requestJson('POST', '/reboot.cgi', { authenticated: true, form: { reboot: 'yes' } }) as IAirosValueMap;
    const okValue = result.ok;
    if (okValue === true || okValue === 'true') {
      return { ...result, success: true, transmitted: true };
    }
    return { ...result, success: false, transmitted: true, error: 'airOS reboot endpoint did not return ok=true.' };
  }

  public async download(): Promise<IAirosValueMap> {
    if (!this.connected) {
      await this.login();
    }
    return this.requestJson('POST', '/api/fw/download', { authenticated: true, json: {} }) as Promise<IAirosValueMap>;
  }

  public async install(): Promise<IAirosValueMap> {
    if (!this.connected) {
      await this.login();
    }
    return this.requestJson('POST', '/fwflash.cgi', { authenticated: true, json: { do_update: 1 } }) as Promise<IAirosValueMap>;
  }

  private async loginV6(): Promise<void> {
    const preflight = await this.request('GET', '/login.cgi', { followRedirects: false });
    this.storeAuth(preflight.headers);
    if (!this.authCookie) {
      throw new AirosApiConnectionError('No airOS v6 session cookie received.');
    }
    const login = await this.request('POST', '/login.cgi', {
      followRedirects: false,
      form: { username: this.options.username || airosDefaultUsername, password: this.options.password, uri: '/index.cgi' },
      headers: { Origin: this.origin(), Referer: this.url('/login.cgi').toString(), Cookie: this.authCookie },
    });
    if (login.statusCode !== 302) {
      throw new AirosApiAuthenticationError('airOS v6 login failed.');
    }
    const activation = await this.request('GET', '/index.cgi', { followRedirects: false, headers: { Referer: this.url('/login.cgi').toString(), Cookie: this.authCookie } });
    if (String(activation.headers.location || '').includes('login.cgi')) {
      throw new AirosApiAuthenticationError('airOS v6 session activation failed.');
    }
    this.connected = true;
  }

  private async requestJson(methodArg: string, pathArg: string, optionsArg: IAirosRequestOptions = {}): Promise<unknown> {
    const response = await this.request(methodArg, pathArg, optionsArg);
    if (optionsArg.isLogin) {
      this.storeAuth(response.headers);
    }
    if (response.statusCode === 401 || response.statusCode === 403) {
      throw new AirosApiAuthenticationError(`airOS request to ${pathArg} failed with HTTP ${response.statusCode}.`);
    }
    if (response.statusCode === 404) {
      throw new AirosApiNotFoundError(`airOS endpoint not found: ${pathArg}.`);
    }
    if (response.statusCode < 200 || response.statusCode >= 300) {
      throw new AirosApiConnectionError(`airOS request to ${pathArg} failed with HTTP ${response.statusCode}.`);
    }
    this.storeAuth(response.headers);
    if (!response.body.trim()) {
      return {};
    }
    try {
      return JSON.parse(response.body) as unknown;
    } catch (errorArg) {
      throw new AirosApiDataError(`airOS endpoint ${pathArg} did not return JSON: ${errorArg instanceof Error ? errorArg.message : String(errorArg)}`);
    }
  }

  private async request(methodArg: string, pathArg: string, optionsArg: IAirosRequestOptions = {}): Promise<IAirosHttpResponse> {
    const targetUrl = this.url(pathArg);
    const body = optionsArg.json !== undefined ? JSON.stringify(optionsArg.json) : optionsArg.form ? new URLSearchParams(Object.entries(optionsArg.form).map(([key, value]) => [key, String(value)])).toString() : undefined;
    const headers: Record<string, string> = { ...(optionsArg.headers || {}) };
    if (optionsArg.json !== undefined) {
      headers['Content-Type'] = 'application/json';
    }
    if (optionsArg.form) {
      headers['Content-Type'] = 'application/x-www-form-urlencoded';
    }
    if (body !== undefined) {
      headers['Content-Length'] = Buffer.byteLength(body).toString();
    }
    if (optionsArg.authenticated) {
      if (!this.connected) {
        throw new AirosApiConnectionError('Not connected to airOS device; login first.');
      }
      if (this.csrfId) {
        headers['X-CSRF-ID'] = this.csrfId;
      }
      if (this.authCookie) {
        headers.Cookie = this.authCookie;
      }
    }

    return new Promise<IAirosHttpResponse>((resolve, reject) => {
      const requestOptions: http.RequestOptions & { rejectUnauthorized?: boolean } = {
        method: methodArg,
        headers,
        timeout: this.options.timeoutMs || airosDefaultTimeoutMs,
      };
      if (targetUrl.protocol === 'https:') {
        requestOptions.rejectUnauthorized = this.options.verifySsl !== false;
      }
      const transport = targetUrl.protocol === 'https:' ? https : http;
      const request = transport.request(targetUrl, requestOptions, (response) => {
        const chunks: Buffer[] = [];
        response.on('data', (chunkArg) => chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
        response.on('end', () => resolve({ statusCode: response.statusCode || 0, headers: response.headers, body: Buffer.concat(chunks).toString('utf8') }));
      });
      request.on('timeout', () => request.destroy(new AirosApiConnectionError(`airOS request to ${targetUrl.toString()} timed out.`)));
      request.on('error', reject);
      if (body !== undefined) {
        request.write(body);
      }
      request.end();
    });
  }

  private storeAuth(headersArg: http.IncomingHttpHeaders): void {
    const csrf = headersArg['x-csrf-id'];
    if (typeof csrf === 'string' && csrf) {
      this.csrfId = csrf;
    }
    const setCookie = headersArg['set-cookie'];
    const cookies = Array.isArray(setCookie) ? setCookie : setCookie ? [setCookie] : [];
    const airosCookie = cookies.map((cookieArg) => cookieArg.split(';', 1)[0]).find((cookieArg) => cookieArg.startsWith('AIROS_') || cookieArg.startsWith('AIROS'));
    if (airosCookie) {
      this.authCookie = airosCookie;
    }
  }

  private resolveBaseUrl(optionsArg: { host: string; port?: number; ssl?: boolean }): URL {
    const ssl = optionsArg.ssl ?? true;
    const base = new URL(optionsArg.host.includes('://') ? optionsArg.host : `${ssl ? 'https' : 'http'}://${optionsArg.host}`);
    if (optionsArg.port && !base.port) {
      base.port = String(optionsArg.port);
    }
    if (!base.port) {
      base.port = String(ssl ? airosDefaultHttpsPort : airosDefaultHttpPort);
    }
    return base;
  }

  private url(pathArg: string): URL {
    return new URL(pathArg, this.baseUrl);
  }

  private origin(): string {
    return `${this.baseUrl.protocol}//${this.baseUrl.host}`;
  }
}

interface IAirosRequestOptions {
  authenticated?: boolean;
  isLogin?: boolean;
  followRedirects?: boolean;
  headers?: Record<string, string>;
  json?: Record<string, unknown>;
  form?: Record<string, unknown>;
}

interface IAirosHttpResponse {
  statusCode: number;
  headers: http.IncomingHttpHeaders;
  body: string;
}
