/**
 * Centralized server configuration management
 * This module provides a single source of truth for the server API base URL
 * and handles automatic port discovery when connections fail.
 *
 * When an explicit URL is configured, port discovery is disabled.
 * Falls back to localhost + port discovery when no explicit URL is provided.
 */

import { tauriReady } from '../tauriShim';

type PortChangeListener = (port: number) => void;
type UrlChangeListener = (url: string, apiKey: string) => void;

class ServerConfig {
  private port: number = 13305;
  private explicitBaseUrl: string | null = null;
  private apiKey: string | null = null;
  private portListeners: Set<PortChangeListener> = new Set();
  private urlListeners: Set<UrlChangeListener> = new Set();
  private isDiscovering: boolean = false;
  private discoveryPromise: Promise<number | null> | null = null;
  private initialized: boolean = false;
  private initPromise: Promise<void> | null = null;

  constructor() {
    // Initialize from the host (Tauri invoke bridge or web-app mock) on startup.
    // Event listeners are registered inside initialize() rather than here
    // because window.api is installed asynchronously by tauriShim.ts and is
    // not yet available during synchronous module-graph evaluation.
    this.initPromise = this.initialize();
  }

  private async initialize(): Promise<void> {
    // Wait for tauriShim.ts to finish installing window.api. Both
    // installTauriApi() and this method are kicked off during module
    // evaluation as fire-and-forget promises; without this await,
    // initialize() races installTauriApi() and loses — every window.api
    // check below sees undefined, and we fall through to localhost:13305
    // with no API key.
    await tauriReady;

    try {
      // Get API Key if available
      if (typeof window !== 'undefined'&& window.api?.getServerAPIKey) {
        this.apiKey = await window.api.getServerAPIKey();
      }

      // In web app mode, use the current origin as the server base URL
      if (typeof window !== 'undefined' && window.api?.isWebApp) {
        const origin = window.location?.origin;
        if (origin && origin !== 'null') {
          const trimmedOrigin = origin.replace(/\/+$/, '');
          console.log('Using web app origin as server base URL:', trimmedOrigin);
          this.explicitBaseUrl = trimmedOrigin;
          this.initialized = true;
          return;
        }
      }

      // Check if an explicit base URL was configured (--base-url or env var)
      if (typeof window !== 'undefined' && window.api?.getServerBaseUrl && window.api?.getServerAPIKey) {
        const baseUrl = await window.api.getServerBaseUrl();
        if (baseUrl) {
          console.log('Using explicit server base URL:', baseUrl);
          this.explicitBaseUrl = baseUrl;
          this.initialized = true;
          return;
        }
      }

      // No explicit URL - use localhost with port discovery
      if (typeof window !== 'undefined' && window.api?.getServerPort) {
        const port = await window.api.getServerPort();
        if (port) {
          this.port = port;
        }
      }

      console.log('Using localhost mode with port:', this.port);
      this.initialized = true;
    } catch (error) {
      console.error('Failed to initialize server config:', error);
      this.initialized = true;
    }

    // Register event listeners AFTER the first await-cycle so window.api is
    // guaranteed to be installed. tauriShim.ts installs window.api via a
    // fire-and-forget async call that completes on the microtask queue; the
    // constructor runs synchronously during module-graph evaluation and would
    // see window.api as undefined if we registered there.
    if (typeof window !== 'undefined' && window.api?.onServerPortUpdated && window.api?.onConnectionSettingsUpdated) {
      window.api.onServerPortUpdated((port: number) => {
        if (!this.explicitBaseUrl) {
          this.setPort(port);
        }
      });

      window.api.onConnectionSettingsUpdated((baseURL: string, apiKey: string) => {
        if (this.explicitBaseUrl != baseURL) {
          this.setUpdatedURL(baseURL);
        }
        if (this.apiKey != apiKey) {
          this.setUpdatedAPIKey(apiKey);
        }
      });
    }
  }

  /**
   * Wait for initialization to complete
   */
  async waitForInit(): Promise<void> {
    if (this.initPromise) {
      await this.initPromise;
    }
  }

  /**
   * Check if using an explicit remote server URL
   */
  isRemoteServer(): boolean {
    return !!this.explicitBaseUrl;
  }

  /**
   * Get the current server port (only meaningful for localhost mode)
   */
  getPort(): number {
    return this.port;
  }

  /**
   * Get the full API base URL
   */
  getApiBaseUrl(): string {
    return `${this.getServerBaseUrl()}/api/v1`;
  }

  /**
   * Get the server base URL (without /api/v1)
   */
  getServerBaseUrl(): string {
    if (this.explicitBaseUrl) {
      return this.explicitBaseUrl;
    }
    return `http://localhost:${this.port}`;
  }

  /**
   * Get the server API key
   */
  getAPIKey(): string {
    if (this.apiKey) {
      return this.apiKey;
    }
    return '';
  }

  /**
   * Build a WebSocket URL. With wsPort, targets the dedicated websocket port
   * advertised by /health; without it, targets the main HTTP port (which
   * accepts WebSocket upgrades for /realtime and /logs/stream). Going through
   * URL rather than string concat is what makes this correct for IPv6
   * literals — URL.host preserves the brackets that hostname does not. The
   * API key is NOT included in the URL — the caller should pass it via
   * Sec-WebSocket-Protocol instead (see websocketClient.ts).
   */
  buildWebSocketUrl(path: string, wsPort?: number, query?: URLSearchParams): string {
    const url = new URL(this.getServerBaseUrl());
    url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
    if (wsPort !== undefined) {
      url.port = String(wsPort);
    }
    url.pathname = url.pathname.replace(/\/$/, '') + path;

    if (query) {
      url.search = query.toString();
    }
    return url.toString();
  }

  /**
   * Set the port and notify all listeners (only for localhost mode)
   */
  private setPort(port: number) {
    if (this.port !== port) {
      console.log(`Server port updated: ${this.port} -> ${port}`);
      this.port = port;
      this.notifyPortListeners();
      this.notifyUrlListeners();
    }
  }

  private setUpdatedURL(baseURL: string | null) {
    const nextBaseUrl = baseURL?.trim() || null;

    if (this.explicitBaseUrl !== nextBaseUrl) {
      console.log(`Base URL updated: ${this.explicitBaseUrl} -> ${nextBaseUrl}`);
      this.explicitBaseUrl = nextBaseUrl;
      this.notifyPortListeners();
      this.notifyUrlListeners();
    }
  }

  private setUpdatedAPIKey(apiKey: string) {
    if (this.apiKey != apiKey) {
      console.log('API Key updated');
      this.apiKey = apiKey;
      this.notifyPortListeners();
      this.notifyUrlListeners();
    }
  }

  /**
   * Discover the server port via a UDP beacon from the local lemond instance.
   * Returns a promise that resolves with the discovered port, or null if discovery is disabled
   */
  async discoverPort(): Promise<number | null> {
    // Skip discovery if using explicit remote URL
    if (this.explicitBaseUrl) {
      console.log('Port discovery skipped - using explicit server URL');
      return null;
    }

    // If already discovering, return the existing promise
    if (this.isDiscovering && this.discoveryPromise) {
      return this.discoveryPromise;
    }

    this.isDiscovering = true;
    this.discoveryPromise = this.performDiscovery();

    try {
      const port = await this.discoveryPromise;
      return port;
    } finally {
      this.isDiscovering = false;
      this.discoveryPromise = null;
    }
  }

  private async performDiscovery(): Promise<number | null> {
    try {
      if (typeof window === 'undefined' || !window.api?.discoverServerPort) {
        console.warn('Port discovery not available');
        return this.port;
      }

      console.log('Discovering server port...');
      const port = await window.api.discoverServerPort();

      // discoverServerPort returns null when explicit URL is configured
      if (port === null) {
        console.log('Port discovery returned null (explicit URL configured)');
        return null;
      }

      this.setPort(port);
      return port;
    } catch (error) {
      console.error('Failed to discover server port:', error);
      return this.port;
    }
  }

  /**
   * Subscribe to port changes (only fires in localhost mode)
   * Returns an unsubscribe function
   */
  onPortChange(listener: PortChangeListener): () => void {
    this.portListeners.add(listener);
    return () => {
      this.portListeners.delete(listener);
    };
  }

  /**
   * Subscribe to URL changes (fires when port changes or explicit URL changes)
   * Returns an unsubscribe function
   */
  onUrlChange(listener: UrlChangeListener): () => void {
    this.urlListeners.add(listener);
    return () => {
      this.urlListeners.delete(listener);
    };
  }

  private notifyPortListeners() {
    this.portListeners.forEach((listener) => {
      try {
        listener(this.port);
      } catch (error) {
        console.error('Error in port change listener:', error);
      }
    });
  }

  private notifyUrlListeners() {
    const url = this.getServerBaseUrl();
    const apiKey = this.getAPIKey();

    this.urlListeners.forEach((listener) => {
      try {
        listener(url, apiKey);
      } catch (error) {
        console.error('Error in URL change listener:', error);
      }
    });
  }

  /**
   * Resolve a relative endpoint against the current base URL. Root-level
   * control endpoints (/internal/*) are not mounted under the /api/v1 prefix,
   * so they resolve against the server base instead.
   */
  private resolveUrl(endpoint: string): string {
    if (endpoint.startsWith('http')) {
      return endpoint;
    }
    const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
    return path.startsWith('/internal/')
      ? `${this.getServerBaseUrl()}${path}`
      : `${this.getApiBaseUrl()}${path}`;
  }

  /**
   * Wrapper for fetch that automatically discovers port on connection failures
   * (only attempts discovery in localhost mode)
   */
  async fetch(endpoint: string, opts?: RequestInit): Promise<Response> {
    await this.waitForInit();

    const fullUrl = this.resolveUrl(endpoint);

    const options = { ...opts };

    if(this.apiKey != null && this.apiKey != '') {
      options.headers = {
        ...options.headers,
        Authorization: `Bearer ${this.apiKey}`,
      }
    }

    try {
      const response = await fetch(fullUrl, options);
      return response;
    } catch (error) {
      // If using explicit URL, don't attempt discovery - just throw
      if (this.explicitBaseUrl) {
        throw error;
      }

      // If fetch fails in localhost mode, try discovering the port and retry once
      console.warn('Fetch failed, attempting port discovery...', error);

      try {
        await this.discoverPort();
        const newUrl = endpoint.startsWith('http')
          ? endpoint.replace(/localhost:\d+/, `localhost:${this.port}`)
          : this.resolveUrl(endpoint);

        return await fetch(newUrl, options);
      } catch (retryError) {
        // If retry also fails, throw the original error
        throw error;
      }
    }
  }
}

// Export singleton instance
export const serverConfig = new ServerConfig();

/**
 * Registered application subprotocol. The server advertises only this protocol,
 * so the client must offer it for libwebsockets to negotiate the upgrade and
 * echo a subprotocol back (browsers fail the socket otherwise).
 */
export const WS_APP_PROTOCOL = 'lemonade-realtime';

function base64UrlEncode(value: string): string {
  const bytes = new TextEncoder().encode(value);
  let binary = '';
  for (const b of bytes) {
    binary += String.fromCharCode(b);
  }
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

/**
 * Build the Sec-WebSocket-Protocol list for an authenticated WebSocket upgrade,
 * shared by the realtime and log-stream clients. Offers the registered
 * application protocol plus a base64url-encoded credential (base64url keeps the
 * key within the token characters a subprotocol value permits). Awaits config
 * initialization so the API key is populated before it is read. Returns
 * undefined when no API key is configured, leaving the upgrade unauthenticated.
 */
export async function webSocketProtocols(): Promise<string[] | undefined> {
  await serverConfig.waitForInit();
  const apiKey = serverConfig.getAPIKey();
  if (!apiKey) {
    return undefined;
  }
  return [WS_APP_PROTOCOL, `bearer.${base64UrlEncode(apiKey)}`];
}

// Export convenience functions
export const getApiBaseUrl = () => serverConfig.getApiBaseUrl();
export const getServerBaseUrl = () => serverConfig.getServerBaseUrl();
export const getAPIKey = () => serverConfig.getAPIKey();
export const getServerPort = () => serverConfig.getPort();
export const discoverServerPort = () => serverConfig.discoverPort();
export const buildWebSocketUrl = (path: string, wsPort?: number, query?: URLSearchParams) =>
  serverConfig.buildWebSocketUrl(path, wsPort, query);
export const isRemoteServer = () => serverConfig.isRemoteServer();
export const onServerPortChange = (listener: PortChangeListener) => serverConfig.onPortChange(listener);
export const onServerUrlChange = (listener: UrlChangeListener) => serverConfig.onUrlChange(listener);
export const serverFetch = (endpoint: string, options?: RequestInit) => serverConfig.fetch(endpoint, options);
