import * as http from 'node:http';
import * as https from 'node:https';
import { ddwrtDefaultHttpPort, ddwrtDefaultHttpsPort, ddwrtDefaultSsl, ddwrtDefaultTimeoutMs, ddwrtDefaultVerifySsl, ddwrtDefaultWirelessOnly, ddwrtLanStatusPath, ddwrtWirelessStatusPath } from './ddwrt.constants.js';
import { DdwrtMapper } from './ddwrt.mapper.js';
import type { IDdwrtCommand, IDdwrtCommandResult, IDdwrtConfig, IDdwrtEvent, IDdwrtNativeClient, IDdwrtRawStatusSet, IDdwrtSnapshot, IDdwrtStatusData } from './ddwrt.types.js';

type TDdwrtEventHandler = (eventArg: IDdwrtEvent) => void;

export class DdwrtApiError extends Error {}
export class DdwrtApiConnectionError extends DdwrtApiError {}
export class DdwrtApiAuthenticationError extends DdwrtApiError {}
export class DdwrtApiDataError extends DdwrtApiError {}

export class DdwrtClient {
  private snapshot?: IDdwrtSnapshot;
  private readonly events: IDdwrtEvent[] = [];
  private readonly eventHandlers = new Set<TDdwrtEventHandler>();

  constructor(private readonly config: IDdwrtConfig) {}

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

    try {
      if (this.config.transport?.readSnapshot) {
        const base = DdwrtMapper.toSnapshot(this.config, { source: 'transport' }, this.events);
        this.snapshot = DdwrtMapper.mergeSnapshot(base, 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.hasManualData()) {
        this.snapshot = DdwrtMapper.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: TDdwrtEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async refresh(): Promise<IDdwrtCommandResult> {
    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, action: 'refresh', data: { snapshot: this.cloneSnapshot(snapshot), source: snapshot.source }, error: success ? undefined : snapshot.error };
  }

  public async sendCommand(commandArg: IDdwrtCommand): Promise<IDdwrtCommandResult> {
    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: IDdwrtCommandResult = { 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<IDdwrtSnapshot> {
    const client = this.localWebClient();
    const wirelessOnly = this.config.wirelessOnly ?? this.config.wireless_only ?? ddwrtDefaultWirelessOnly;
    const requiredStatus = wirelessOnly ? await client.getWirelessStatus() : await client.getLanStatus();
    const optionalStatus = wirelessOnly
      ? await client.getLanStatus().catch(() => undefined)
      : await client.getWirelessStatus().catch(() => undefined);
    const rawStatus: IDdwrtRawStatusSet = wirelessOnly
      ? { wireless: requiredStatus, lan: optionalStatus }
      : { wireless: optionalStatus, lan: requiredStatus };
    return DdwrtMapper.toSnapshot({ ...this.config, rawStatus }, { source: 'http', connected: true, router: { authenticated: true } }, this.events);
  }

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

  private async snapshotFromNativeClient(clientArg: IDdwrtNativeClient): Promise<IDdwrtSnapshot> {
    await clientArg.login?.();
    if (clientArg.getSnapshot) {
      const result = await clientArg.getSnapshot();
      if (this.isSnapshot(result)) {
        return DdwrtMapper.toSnapshot({ ...this.config, snapshot: result }, { source: 'client', connected: result.connected }, this.events);
      }
      return DdwrtMapper.mergeSnapshot(DdwrtMapper.toSnapshot(this.config, { source: 'client', connected: true }, this.events), result);
    }

    const rawStatus = await this.rawStatusFromNativeClient(clientArg);
    return DdwrtMapper.toSnapshot({ ...this.config, rawStatus }, { source: 'client', connected: true, router: { authenticated: true } }, this.events);
  }

  private async rawStatusFromNativeClient(clientArg: IDdwrtNativeClient): Promise<IDdwrtRawStatusSet> {
    if (clientArg.getStatus) {
      const status = await clientArg.getStatus();
      if (this.isRawStatusSet(status)) {
        return {
          wireless: DdwrtMapper.toStatusData(status.wireless),
          lan: DdwrtMapper.toStatusData(status.lan),
          wirelessResponse: status.wirelessResponse,
          lanResponse: status.lanResponse,
        };
      }
      return { wireless: DdwrtMapper.toStatusData(status) };
    }
    const wireless = clientArg.getWirelessStatus ? DdwrtMapper.toStatusData(await clientArg.getWirelessStatus()) : undefined;
    const lan = clientArg.getLanStatus ? DdwrtMapper.toStatusData(await clientArg.getLanStatus()) : undefined;
    if (!wireless && !lan) {
      throw new DdwrtApiConnectionError('DD-WRT nativeClient must expose getSnapshot(), getStatus(), getWirelessStatus(), or getLanStatus().');
    }
    return { wireless, lan };
  }

  private async executeCommand(commandArg: IDdwrtCommand): Promise<IDdwrtCommandResult> {
    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: 'transport' }), { ...commandArg, method: 'transport' }, true);
    }
    if (this.config.nativeClient) {
      return this.executeNativeClientCommand(this.config.nativeClient, commandArg);
    }
    return {
      success: false,
      transmitted: false,
      action: commandArg.action,
      error: 'DD-WRT commands require an injected nativeClient, commandExecutor, or transport.execute. Native HTTP status pages and static snapshots are read-only.',
      data: { command: commandArg },
    };
  }

  private async executeNativeClientCommand(clientArg: IDdwrtNativeClient, commandArg: IDdwrtCommand): Promise<IDdwrtCommandResult> {
    await clientArg.login?.();
    if (clientArg.execute) {
      return this.commandResult(await clientArg.execute({ ...commandArg, method: 'native_client' }), { ...commandArg, method: 'native_client' }, true);
    }
    if (clientArg.sendCommand) {
      return this.commandResult(await clientArg.sendCommand({ ...commandArg, method: 'native_client' }), { ...commandArg, method: 'native_client' }, true);
    }
    if (commandArg.action === 'reboot' && clientArg.reboot) {
      return this.commandResult(await clientArg.reboot(), { ...commandArg, method: 'native_client' }, true);
    }
    return { success: false, transmitted: false, action: commandArg.action, error: `DD-WRT nativeClient does not implement ${commandArg.action}.`, data: { command: commandArg } };
  }

  private commandResult(resultArg: unknown, commandArg: IDdwrtCommand, transmittedDefaultArg: boolean): IDdwrtCommandResult {
    if (this.isCommandResult(resultArg)) {
      return { transmitted: transmittedDefaultArg, action: commandArg.action, ...resultArg };
    }
    if (resultArg === false) {
      return { success: false, transmitted: transmittedDefaultArg, action: commandArg.action, error: 'DD-WRT 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.success === false || record.error) {
        return { success: false, transmitted: transmittedDefaultArg, action: commandArg.action, error: String(record.error || 'DD-WRT command returned failure.'), data: { command: commandArg, response: resultArg } };
      }
    }
    return { success: true, transmitted: transmittedDefaultArg, action: commandArg.action, data: { command: commandArg, response: resultArg } };
  }

  private patchSnapshot(commandArg: IDdwrtCommand, resultArg: IDdwrtCommandResult): void {
    const current = this.snapshot || DdwrtMapper.toSnapshot(this.config, {}, this.events);
    this.snapshot = DdwrtMapper.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: IDdwrtCommand) => Promise<IDdwrtCommandResult | 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 hasManualData(): boolean {
    return Boolean(this.config.rawStatus || this.config.wirelessStatus || this.config.lanStatus || this.config.wirelessResponse || this.config.lanResponse || this.config.clients?.length || this.config.leases?.length || this.config.sensors);
  }

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

  private localWebClient(): DdwrtLocalStatusClient {
    return new DdwrtLocalStatusClient({
      host: this.config.host as string,
      port: this.config.port,
      username: this.config.username as string,
      password: this.config.password as string,
      ssl: this.config.ssl ?? this.config.useSsl ?? ddwrtDefaultSsl,
      verifySsl: this.config.verifySsl ?? this.config.verify_ssl ?? ddwrtDefaultVerifySsl,
      timeoutMs: this.config.timeoutMs || ddwrtDefaultTimeoutMs,
    });
  }

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

  private unsupportedReadMessage(): string {
    return 'No DD-WRT host/username/password native HTTP, nativeClient, transport.readSnapshot, snapshot, or live status data is configured.';
  }

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

  private isRawStatusSet(valueArg: unknown): valueArg is IDdwrtRawStatusSet {
    return Boolean(valueArg && typeof valueArg === 'object' && ('wireless' in valueArg || 'lan' in valueArg || 'wirelessResponse' in valueArg || 'lanResponse' in valueArg));
  }

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

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

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

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

export class DdwrtLocalStatusClient implements IDdwrtNativeClient {
  private readonly baseUrl: URL;

  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 getSnapshot(): Promise<IDdwrtSnapshot> {
    const wireless = await this.getWirelessStatus();
    const lan = await this.getLanStatus().catch(() => undefined);
    return DdwrtMapper.toSnapshot({
      host: this.options.host,
      port: this.options.port,
      username: this.options.username,
      ssl: this.options.ssl,
      verifySsl: this.options.verifySsl,
      rawStatus: { wireless, lan },
    }, { source: 'http', connected: true, router: { authenticated: true } });
  }

  public async getWirelessStatus(): Promise<IDdwrtStatusData> {
    return this.requestStatus(ddwrtWirelessStatusPath);
  }

  public async getLanStatus(): Promise<IDdwrtStatusData> {
    return this.requestStatus(ddwrtLanStatusPath);
  }

  private async requestStatus(pathArg: string): Promise<IDdwrtStatusData> {
    const response = await this.request('GET', pathArg);
    if (response.statusCode === 401 || response.statusCode === 403) {
      throw new DdwrtApiAuthenticationError(`DD-WRT request to ${pathArg} failed with HTTP ${response.statusCode}.`);
    }
    if (response.statusCode < 200 || response.statusCode >= 300) {
      throw new DdwrtApiConnectionError(`DD-WRT request to ${pathArg} failed with HTTP ${response.statusCode}.`);
    }
    const parsed = DdwrtMapper.parseDdwrtResponse(response.body);
    if (!Object.keys(parsed).length) {
      throw new DdwrtApiDataError(`DD-WRT endpoint ${pathArg} did not return live status data.`);
    }
    return parsed;
  }

  private async request(methodArg: string, pathArg: string): Promise<IDdwrtHttpResponse> {
    const targetUrl = this.url(pathArg);
    const headers: Record<string, string> = {
      Authorization: `Basic ${Buffer.from(`${this.options.username}:${this.options.password}`).toString('base64')}`,
    };
    return new Promise<IDdwrtHttpResponse>((resolve, reject) => {
      const requestOptions: http.RequestOptions & { rejectUnauthorized?: boolean } = {
        method: methodArg,
        headers,
        timeout: this.options.timeoutMs || ddwrtDefaultTimeoutMs,
      };
      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 DdwrtApiConnectionError(`DD-WRT request to ${targetUrl.toString()} timed out.`)));
      request.on('error', reject);
      request.end();
    });
  }

  private resolveBaseUrl(optionsArg: { host: string; port?: number; ssl?: boolean }): URL {
    const ssl = optionsArg.ssl ?? ddwrtDefaultSsl;
    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(base.protocol === 'https:' ? ddwrtDefaultHttpsPort : ddwrtDefaultHttpPort);
    }
    return base;
  }

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

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