import * as plugins from '../../plugins.js';
import type { IServiceCallResult } from '../../core/types.js';
import { FritzboxCallmonitorMapper } from './fritzbox_callmonitor.mapper.js';
import type {
  IFritzboxCallmonitorCallEvent,
  IFritzboxCallmonitorClientEvent,
  IFritzboxCallmonitorConfig,
  IFritzboxCallmonitorEventStream,
  IFritzboxCallmonitorSnapshot,
  TFritzboxCallmonitorEventInput,
  TFritzboxCallmonitorSnapshotSource,
  TFritzboxCallmonitorUnsubscribe,
} from './fritzbox_callmonitor.types.js';
import { fritzboxCallmonitorDefaultPort } from './fritzbox_callmonitor.types.js';

type TFritzboxCallmonitorClientEventHandler = (eventArg: IFritzboxCallmonitorClientEvent) => void;

export class FritzboxCallmonitorClient {
  private currentSnapshot?: IFritzboxCallmonitorSnapshot;
  private readonly eventHandlers = new Set<TFritzboxCallmonitorClientEventHandler>();
  private socket?: plugins.net.Socket;
  private socketBuffer = '';
  private monitoring = false;
  private stopMonitoringHandler?: TFritzboxCallmonitorUnsubscribe;

  constructor(private readonly config: IFritzboxCallmonitorConfig) {}

  public async getSnapshot(): Promise<IFritzboxCallmonitorSnapshot> {
    if (this.config.nativeClient?.getSnapshot) {
      this.currentSnapshot = FritzboxCallmonitorMapper.normalizeSnapshot(await this.config.nativeClient.getSnapshot(), this.config, 'provider');
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.snapshotProvider) {
      const provided = await this.config.snapshotProvider();
      if (provided) {
        this.currentSnapshot = FritzboxCallmonitorMapper.normalizeSnapshot(provided, this.config, 'provider');
        return this.cloneSnapshot(this.currentSnapshot);
      }
    }

    if (!this.currentSnapshot) {
      this.currentSnapshot = FritzboxCallmonitorMapper.toSnapshot(this.config);
    }
    return this.cloneSnapshot(this.currentSnapshot);
  }

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

  public async refresh(): Promise<IServiceCallResult> {
    try {
      this.currentSnapshot = undefined;
      const snapshot = await this.getSnapshot();
      this.emit({ type: 'snapshot_refreshed', snapshot, timestamp: Date.now() });
      return { success: true, data: snapshot };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = FritzboxCallmonitorMapper.toSnapshot({ ...this.config, snapshot: this.currentSnapshot, connected: false }, false);
      this.currentSnapshot = snapshot;
      this.emit({ type: 'monitor_failed', snapshot, error, timestamp: Date.now() });
      return { success: false, error, data: snapshot };
    }
  }

  public async processEvent(eventArg: TFritzboxCallmonitorEventInput, sourceArg: TFritzboxCallmonitorSnapshotSource = 'manual'): Promise<IServiceCallResult> {
    try {
      const snapshot = await this.applyEvent(eventArg, sourceArg);
      const callEvent = snapshot.call.lastEvent;
      this.emit({ type: 'event_received', snapshot, callEvent, raw: typeof eventArg === 'string' ? eventArg : eventArg.raw, timestamp: Date.now() });
      return { success: true, data: { event: callEvent, snapshot } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = await this.getSnapshot().catch(() => undefined);
      this.emit({ type: 'event_parse_failed', snapshot, raw: typeof eventArg === 'string' ? eventArg : eventArg.raw, error, timestamp: Date.now() });
      return { success: false, error, data: snapshot };
    }
  }

  public async startMonitoring(): Promise<IServiceCallResult> {
    if (this.monitoring) {
      return { success: true, data: await this.getSnapshot() };
    }

    if (this.config.nativeClient?.subscribe) {
      const unsubscribe = await this.config.nativeClient.subscribe((eventArg) => void this.processEvent(eventArg, 'provider'));
      this.stopMonitoringHandler = this.unsubscribeHandler(unsubscribe);
      this.monitoring = true;
      const snapshot = await this.markConnected('provider');
      this.emit({ type: 'monitor_started', snapshot, timestamp: Date.now() });
      return { success: true, data: snapshot };
    }

    if (this.config.eventSource) {
      const unsubscribe = await this.config.eventSource.subscribe((eventArg) => void this.processEvent(eventArg, 'provider'));
      this.stopMonitoringHandler = this.unsubscribeHandler(unsubscribe);
      this.monitoring = true;
      const snapshot = await this.markConnected('provider');
      this.emit({ type: 'monitor_started', snapshot, timestamp: Date.now() });
      return { success: true, data: snapshot };
    }

    if (this.config.eventExecutor) {
      return this.startExecutorMonitoring();
    }

    if (this.config.host) {
      return this.startTcpMonitoring(this.config.host, this.config.port || fritzboxCallmonitorDefaultPort);
    }

    const snapshot = await this.getSnapshot();
    const error = 'FRITZ!Box call monitor live events require config.host, nativeClient.subscribe, eventSource, or eventExecutor.';
    this.emit({ type: 'monitor_failed', snapshot, error, timestamp: Date.now() });
    return { success: false, error, data: snapshot };
  }

  public async stopMonitoring(): Promise<IServiceCallResult> {
    const wasMonitoring = this.monitoring;
    this.monitoring = false;

    if (this.stopMonitoringHandler) {
      const stop = this.stopMonitoringHandler;
      this.stopMonitoringHandler = undefined;
      await stop();
    }

    if (this.socket) {
      const socket = this.socket;
      this.socket = undefined;
      socket.removeAllListeners();
      socket.end();
      socket.destroy();
    }

    const snapshot = await this.markConnected(this.currentSnapshot?.source || 'runtime', wasMonitoring ? false : this.currentSnapshot?.connected ?? false);
    if (wasMonitoring) {
      this.emit({ type: 'monitor_stopped', snapshot, timestamp: Date.now() });
    }
    return { success: true, data: snapshot };
  }

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

  private async startExecutorMonitoring(): Promise<IServiceCallResult> {
    const host = this.config.host;
    const port = this.config.port || fritzboxCallmonitorDefaultPort;
    try {
      const stream = await this.config.eventExecutor?.({
        host,
        port,
        onLine: (lineArg) => void this.processEvent(lineArg, 'provider'),
        onError: (errorArg) => void this.handleMonitoringError(errorArg),
      });
      if (this.isServiceResult(stream) && !stream.success) {
        const snapshot = await this.getSnapshot();
        this.emit({ type: 'monitor_failed', snapshot, error: stream.error, data: stream.data, timestamp: Date.now() });
        return { success: false, error: stream.error, data: stream.data || snapshot };
      }
      this.stopMonitoringHandler = this.streamStopHandler(stream);
      this.monitoring = true;
      const snapshot = await this.markConnected('provider');
      this.emit({ type: 'monitor_started', snapshot, timestamp: Date.now() });
      return { success: true, data: snapshot };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = await this.markConnected('provider', false, error);
      this.emit({ type: 'monitor_failed', snapshot, error, timestamp: Date.now() });
      return { success: false, error, data: snapshot };
    }
  }

  private async startTcpMonitoring(hostArg: string, portArg: number): Promise<IServiceCallResult> {
    return new Promise<IServiceCallResult>((resolve) => {
      let settled = false;
      let stoppedByClient = false;
      const timeoutMs = this.config.timeoutMs || 10_000;
      const socket = plugins.net.createConnection({ host: hostArg, port: portArg });
      const timer = setTimeout(() => fail(new Error(`FRITZ!Box call monitor TCP connection to ${hostArg}:${portArg} timed out after ${timeoutMs}ms.`)), timeoutMs);
      this.socket = socket;

      const cleanupConnect = () => clearTimeout(timer);
      const fail = (errorArg: Error) => {
        if (settled) {
          this.handleMonitoringError(errorArg);
          return;
        }
        settled = true;
        cleanupConnect();
        socket.removeAllListeners();
        socket.destroy();
        this.socket = undefined;
        void this.markConnected('tcp', false, this.errorMessage(errorArg)).then((snapshotArg) => {
          this.emit({ type: 'monitor_failed', snapshot: snapshotArg, error: this.errorMessage(errorArg), timestamp: Date.now() });
          resolve({ success: false, error: this.errorMessage(errorArg), data: snapshotArg });
        });
      };

      socket.on('connect', () => {
        settled = true;
        cleanupConnect();
        this.monitoring = true;
        void this.markConnected('tcp', true).then((snapshotArg) => {
          this.emit({ type: 'monitor_started', snapshot: snapshotArg, timestamp: Date.now() });
          resolve({ success: true, data: snapshotArg });
        });
      });
      socket.on('data', (chunkArg) => this.handleTcpData(chunkArg.toString('utf8')));
      socket.on('error', (errorArg) => fail(errorArg));
      socket.on('close', () => {
        cleanupConnect();
        if (!settled) {
          fail(new Error(`FRITZ!Box call monitor TCP connection to ${hostArg}:${portArg} closed before connecting.`));
          return;
        }
        this.monitoring = false;
        this.socket = undefined;
        if (!stoppedByClient) {
          void this.markConnected('tcp', false).then((snapshotArg) => this.emit({ type: 'monitor_stopped', snapshot: snapshotArg, timestamp: Date.now() }));
        }
      });
      this.stopMonitoringHandler = async () => {
        stoppedByClient = true;
        socket.removeAllListeners('error');
        socket.end();
        socket.destroy();
      };
    });
  }

  private handleTcpData(chunkArg: string): void {
    this.socketBuffer += chunkArg;
    const lines = this.socketBuffer.split(/\r?\n/);
    this.socketBuffer = lines.pop() || '';
    for (const line of lines.map((lineArg) => lineArg.trim()).filter(Boolean)) {
      void this.processEvent(line, 'tcp');
    }
  }

  private async applyEvent(eventArg: TFritzboxCallmonitorEventInput, sourceArg: TFritzboxCallmonitorSnapshotSource): Promise<IFritzboxCallmonitorSnapshot> {
    const snapshot = await this.getSnapshot();
    const callEvent = FritzboxCallmonitorMapper.parseEvent(eventArg, snapshot.contacts, snapshot.prefixes || []);
    this.currentSnapshot = this.snapshotWithEvent(snapshot, callEvent, sourceArg);
    return this.cloneSnapshot(this.currentSnapshot);
  }

  private snapshotWithEvent(snapshotArg: IFritzboxCallmonitorSnapshot, eventArg: IFritzboxCallmonitorCallEvent, sourceArg: TFritzboxCallmonitorSnapshotSource): IFritzboxCallmonitorSnapshot {
    const events = [...snapshotArg.events, eventArg].slice(-50);
    return {
      ...snapshotArg,
      connected: sourceArg === 'tcp' || sourceArg === 'provider' || snapshotArg.connected || sourceArg === 'manual',
      source: sourceArg,
      updatedAt: new Date().toISOString(),
      call: {
        state: eventArg.callState,
        attributes: eventArg.attributes,
        lastEvent: eventArg,
      },
      events,
      error: undefined,
    };
  }

  private async markConnected(sourceArg: TFritzboxCallmonitorSnapshotSource, connectedArg = true, errorArg?: string): Promise<IFritzboxCallmonitorSnapshot> {
    const snapshot = await this.getSnapshot();
    this.currentSnapshot = {
      ...snapshot,
      connected: connectedArg,
      source: sourceArg,
      updatedAt: new Date().toISOString(),
      error: errorArg,
    };
    return this.cloneSnapshot(this.currentSnapshot);
  }

  private handleMonitoringError(errorArg: Error): void {
    void this.markConnected(this.currentSnapshot?.source || 'runtime', false, this.errorMessage(errorArg)).then((snapshotArg) => {
      this.emit({ type: 'monitor_failed', snapshot: snapshotArg, error: this.errorMessage(errorArg), timestamp: Date.now() });
    });
  }

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

  private unsubscribeHandler(valueArg: TFritzboxCallmonitorUnsubscribe | void): TFritzboxCallmonitorUnsubscribe | undefined {
    return typeof valueArg === 'function' ? valueArg : undefined;
  }

  private streamStopHandler(valueArg: IFritzboxCallmonitorEventStream | IServiceCallResult | void): TFritzboxCallmonitorUnsubscribe | undefined {
    if (valueArg && typeof valueArg === 'object' && 'stop' in valueArg && typeof valueArg.stop === 'function') {
      return () => valueArg.stop?.();
    }
    return undefined;
  }

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

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

  private cloneSnapshot<T extends IFritzboxCallmonitorSnapshot>(snapshotArg: T): T {
    return JSON.parse(JSON.stringify(snapshotArg)) as T;
  }
}
