/**
 * RVS (Rendezvous Server) - WebSocket-Verbindungsmanager
 *
 * Verwaltet die persistente WebSocket-Verbindung zwischen der ARIA Cockpit App
 * und dem Rendezvous Server. Unterstützt Auto-Reconnect, Heartbeat und
 * typisierte Nachrichten.
 */

import AsyncStorage from '@react-native-async-storage/async-storage';

// --- Typen ---

export type ConnectionState = 'connecting' | 'connected' | 'disconnected';

export type MessageType = 'chat' | 'audio' | 'file' | 'location' | 'mode' | 'log' | 'event' | 'update_available' | string;

export interface RVSMessage {
  type: MessageType;
  payload: Record<string, unknown>;
  timestamp: number;
}

export interface ConnectionConfig {
  host: string;
  port: number;
  token: string;
  useTLS: boolean;
}

type MessageCallback = (message: RVSMessage) => void;
type StateCallback = (state: ConnectionState) => void;

/** Einzelner Eintrag im Verbindungslog */
export interface ConnectionLogEntry {
  timestamp: number;
  level: 'info' | 'warn' | 'error';
  message: string;
}

type LogCallback = (entry: ConnectionLogEntry) => void;

// --- Konstanten ---

const HEARTBEAT_INTERVAL_MS = 25_000;
const INITIAL_RECONNECT_DELAY_MS = 1_000;
const MAX_RECONNECT_DELAY_MS = 30_000;
const RECONNECT_BACKOFF_FACTOR = 2;
const MAX_LOG_ENTRIES = 100;

// --- RVS-Klasse ---

class RVSConnection {
  private ws: WebSocket | null = null;
  private config: ConnectionConfig | null = null;
  private state: ConnectionState = 'disconnected';

  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private reconnectDelay: number = INITIAL_RECONNECT_DELAY_MS;
  private shouldReconnect: boolean = false;

  private messageListeners: MessageCallback[] = [];
  private stateListeners: StateCallback[] = [];
  private logListeners: LogCallback[] = [];
  private connectionLog: ConnectionLogEntry[] = [];
  private usingTLSFallback: boolean = false;

  // --- Konfiguration ---

  /** Verbindungsdaten setzen (z.B. nach QR-Scan) */
  setConfig(config: ConnectionConfig): void {
    this.config = config;
    this.saveConfig(config);
  }

  getConfig(): ConnectionConfig | null {
    return this.config;
  }

  getState(): ConnectionState {
    return this.state;
  }

  // --- Verbindung ---

  /** Verbindung zum RVS aufbauen. force=true: bestehende Connection hart
   * schliessen + neu verbinden (auch wenn JS denkt readyState=OPEN — kann
   * nach Hintergrund-Pause ein Zombie-WS sein wo TCP tot ist aber JS-State
   * noch OPEN zeigt; in dem Fall war "Bereits verbunden" ein No-Op und
   * Stefan musste manuell zigmal klicken). */
  connect(force: boolean = false): void {
    if (!this.config) {
      this.log('warn', 'Keine Verbindungskonfiguration vorhanden');
      return;
    }

    if (!force && this.ws?.readyState === WebSocket.OPEN) {
      this.log('info', 'Bereits verbunden');
      return;
    }

    // Wenn ein WS-Objekt da ist (Zombie oder lebend), sauber abreissen
    // bevor wir einen neuen aufbauen — sonst gibt's zwei parallele
    // Verbindungen + doppelte Events.
    if (this.ws) {
      this.log('info', 'Bestehende WS-Verbindung wird geschlossen vor Neu-Connect');
      try {
        this.ws.onclose = null;  // verhindert dass scheduleReconnect doppelt feuert
        this.ws.onerror = null;
        this.ws.close();
      } catch (_) {}
      this.ws = null;
    }

    this.shouldReconnect = true;
    this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
    this.usingTLSFallback = false;
    this.clearTimers();
    this.log('info', `Verbindungsaufbau zu ${this.config.host}:${this.config.port} (TLS: ${this.config.useTLS ? 'ja' : 'nein'})`);
    this.establishConnection();
  }

  /** Verbindung trennen (kein Auto-Reconnect) */
  disconnect(): void {
    this.shouldReconnect = false;
    this.clearTimers();

    if (this.ws) {
      this.ws.close(1000, 'Benutzer hat getrennt');
      this.ws = null;
    }

    this.log('info', 'Verbindung getrennt (manuell)');
    this.setState('disconnected');
  }

  /** Nachricht an den RVS senden */
  send(type: MessageType, payload: Record<string, unknown>): void {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      console.warn('[RVS] Kann nicht senden - nicht verbunden');
      return;
    }

    const message: RVSMessage = {
      type,
      payload,
      timestamp: Date.now(),
    };

    this.ws.send(JSON.stringify(message));
  }

  // --- Event-Listener ---

  /** Callback fuer eingehende Nachrichten registrieren */
  onMessage(callback: MessageCallback): () => void {
    this.messageListeners.push(callback);
    // Gibt Unsubscribe-Funktion zurueck
    return () => {
      this.messageListeners = this.messageListeners.filter(cb => cb !== callback);
    };
  }

  /** Callback fuer Verbindungsstatus-Aenderungen registrieren */
  onStateChange(callback: StateCallback): () => void {
    this.stateListeners.push(callback);
    return () => {
      this.stateListeners = this.stateListeners.filter(cb => cb !== callback);
    };
  }

  /** Callback fuer Verbindungslog-Eintraege registrieren */
  onLog(callback: LogCallback): () => void {
    this.logListeners.push(callback);
    return () => {
      this.logListeners = this.logListeners.filter(cb => cb !== callback);
    };
  }

  /** Gesamten Verbindungslog abrufen */
  getConnectionLog(): ConnectionLogEntry[] {
    return [...this.connectionLog];
  }

  // --- Interne Methoden ---

  /** Eintrag ins Verbindungslog schreiben */
  private log(level: ConnectionLogEntry['level'], message: string): void {
    const entry: ConnectionLogEntry = { timestamp: Date.now(), level, message };
    this.connectionLog = [...this.connectionLog.slice(-(MAX_LOG_ENTRIES - 1)), entry];
    this.logListeners.forEach(cb => cb(entry));
    const prefix = level === 'error' ? 'ERROR' : level === 'warn' ? 'WARN' : 'INFO';
    console.log(`[RVS] [${prefix}] ${message}`);
  }

  private establishConnection(): void {
    if (!this.config) return;

    this.setState('connecting');

    const useTLS = this.config.useTLS && !this.usingTLSFallback;
    const protocol = useTLS ? 'wss' : 'ws';
    const url = `${protocol}://${this.config.host}:${this.config.port}?token=${this.config.token}`;

    this.log('info', `Verbinde: ${protocol}://${this.config.host}:${this.config.port}`);

    try {
      this.ws = new WebSocket(url);

      this.ws.onopen = () => {
        const tlsInfo = this.usingTLSFallback ? ' (TLS-Fallback: ws://)' : '';
        this.log('info', `Verbunden${tlsInfo}`);
        this.setState('connected');
        this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
        this.startHeartbeat();
      };

      this.ws.onmessage = (event: WebSocketMessageEvent) => {
        try {
          const message: RVSMessage = JSON.parse(event.data as string);
          this.notifyMessageListeners(message);
        } catch (err) {
          this.log('error', `Nachricht parsen fehlgeschlagen: ${err}`);
        }
      };

      this.ws.onclose = (event) => {
        this.log('info', `Verbindung geschlossen (Code: ${event.code}, Reason: ${event.reason || '-'})`);
        this.clearTimers();
        this.ws = null;
        this.setState('disconnected');

        // Sticky-Fallback-Reset: beim naechsten Reconnect wieder primary
        // (wss://) versuchen statt fuer immer auf ws:// zu kleben. War
        // der Hauptgrund warum die App nach Hintergrund-Rueckkehr nicht
        // mehr verband — TLS-Handshake-Timeout in einem Reconnect → Fallback
        // auf ws:// → Caddy refused → endlos im Fallback haengen.
        if (this.usingTLSFallback) {
          this.log('info', 'Reset TLS-Fallback fuer naechsten Reconnect (zurueck zu wss://)');
          this.usingTLSFallback = false;
        }

        if (this.shouldReconnect) {
          this.scheduleReconnect();
        }
      };

      this.ws.onerror = (error) => {
        const errorMsg = (error as any)?.message || 'Unbekannter Fehler';
        this.log('error', `WebSocket-Fehler: ${errorMsg}`);

        // TLS-Fallback: Wenn wss:// fehlschlaegt, auf ws:// wechseln
        if (this.config?.useTLS && !this.usingTLSFallback) {
          this.usingTLSFallback = true;
          // shouldReconnect kurz deaktivieren damit onclose keinen
          // parallelen Reconnect ausloest — wir machen das selbst
          this.shouldReconnect = false;
          this.log('warn', 'TLS fehlgeschlagen — Fallback auf ws:// (ohne TLS)');
          this.clearTimers();
          if (this.ws) {
            this.ws.onclose = null; // onclose-Handler entfernen um Doppel-Reconnect zu verhindern
            try { this.ws.close(); } catch (_) {}
          }
          this.ws = null;
          this.shouldReconnect = true;
          this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
          this.establishConnection();
          return;
        }
      };
    } catch (err) {
      this.log('error', `Verbindungsfehler: ${err}`);
      this.setState('disconnected');

      if (this.shouldReconnect) {
        this.scheduleReconnect();
      }
    }
  }

  /** Reconnect mit exponentiellem Backoff planen */
  private scheduleReconnect(): void {
    this.log('info', `Reconnect in ${this.reconnectDelay / 1000}s...`);

    this.reconnectTimer = setTimeout(() => {
      this.establishConnection();
    }, this.reconnectDelay);

    // Exponentieller Backoff: 1s -> 2s -> 4s -> 8s -> ... -> max 30s
    this.reconnectDelay = Math.min(
      this.reconnectDelay * RECONNECT_BACKOFF_FACTOR,
      MAX_RECONNECT_DELAY_MS,
    );
  }

  /** Heartbeat starten (alle 25 Sekunden) */
  private startHeartbeat(): void {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() }));
      }
    }, HEARTBEAT_INTERVAL_MS);
  }

  private clearTimers(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }

  private setState(state: ConnectionState): void {
    if (this.state !== state) {
      this.state = state;
      this.stateListeners.forEach(cb => cb(state));
    }
  }

  private notifyMessageListeners(message: RVSMessage): void {
    this.messageListeners.forEach(cb => cb(message));
  }

  // --- Persistenz ---

  private static readonly STORAGE_KEY = 'rvs_config';

  private async saveConfig(config: ConnectionConfig): Promise<void> {
    try {
      await AsyncStorage.setItem(RVSConnection.STORAGE_KEY, JSON.stringify(config));
      console.log('[RVS] Konfiguration gespeichert');
    } catch (err) {
      console.error('[RVS] Fehler beim Speichern:', err);
    }
  }

  async loadConfig(): Promise<ConnectionConfig | null> {
    try {
      const data = await AsyncStorage.getItem(RVSConnection.STORAGE_KEY);
      if (data) {
        this.config = JSON.parse(data);
        console.log('[RVS] Konfiguration geladen');
        return this.config;
      }
      return null;
    } catch (err) {
      console.error('[RVS] Fehler beim Laden:', err);
      return null;
    }
  }
}

// Singleton-Instanz
const rvs = new RVSConnection();
export default rvs;
