/**
 * SIP proxy bootstrap.
 *
 * Spawns the Rust proxy-engine, wires runtime state/event handling,
 * and starts the web dashboard plus browser signaling layer.
 */

import fs from 'node:fs';
import path from 'node:path';

import { applyConfigUpdates, type IAppConfig } from './config.ts';
import { FaxBoxManager } from './faxbox.ts';
import { FaxJobManager } from './faxjobs.ts';
import { broadcastWs, initWebUi } from './frontend.ts';
import { initWebRtcSignaling, getAllBrowserDeviceIds, sendToBrowserDevice } from './webrtcbridge.ts';
import { VoiceboxManager } from './voicebox.ts';
import {
  initProxyEngine,
  configureProxyEngine,
  hangupCall,
  makeCall,
  shutdownProxyEngine,
  webrtcOffer,
  webrtcIce,
  webrtcLink,
  webrtcClose,
} from './proxybridge.ts';
import { registerProxyEventHandlers } from './runtime/proxy-events.ts';
import { StatusStore } from './runtime/status-store.ts';
import { SiprouterStorage } from './storage.ts';
import { WebRtcLinkManager, type IProviderMediaInfo } from './runtime/webrtc-linking.ts';

let appConfig: IAppConfig;

const LOG_PATH = path.join(process.cwd(), 'sip_trace.log');
const startTime = Date.now();
const instanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

const storage = new SiprouterStorage(log);
let statusStore: StatusStore;
let webRtcLinks: WebRtcLinkManager;
let faxBoxManager: FaxBoxManager;
let faxJobManager: FaxJobManager;
let voiceboxManager: VoiceboxManager;

function now(): string {
  return new Date().toISOString().replace('T', ' ').slice(0, 19);
}

function log(message: string): void {
  const line = `${now()} ${message}\n`;
  fs.appendFileSync(LOG_PATH, line);
  process.stdout.write(line);
  broadcastWs('log', { message });
}

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

function buildProxyConfig(config: IAppConfig): Record<string, unknown> {
  return {
    proxy: config.proxy,
    providers: config.providers,
    devices: config.devices,
    routing: config.routing,
    faxboxes: config.faxboxes ?? [],
    voiceboxes: config.voiceboxes ?? [],
    ivr: config.ivr,
  };
}

function getStatus() {
  return statusStore.buildStatusSnapshot(
    instanceId,
    startTime,
    getAllBrowserDeviceIds(),
    voiceboxManager.getAllUnheardCounts(),
  );
}

function requestWebRtcLink(callId: string, sessionId: string, media: IProviderMediaInfo): void {
  log(`[webrtc] linking session=${sessionId.slice(0, 8)} to call=${callId} media=${media.addr}:${media.port} pt=${media.sipPt}`);
  void webrtcLink(sessionId, callId, media.addr, media.port, media.sipPt).then((ok) => {
    log(`[webrtc] link result: ${ok}`);
  });
}

async function configureRuntime(config: IAppConfig): Promise<boolean> {
  return configureProxyEngine(buildProxyConfig(config));
}

async function reloadConfig(): Promise<void> {
  try {
    const previousConfig = appConfig;
    const nextConfig = await storage.getAppConfig();

    appConfig = nextConfig;
    statusStore.updateConfig(nextConfig);
    await faxBoxManager.init(nextConfig.faxboxes ?? []);
    await voiceboxManager.init(nextConfig.voiceboxes ?? []);

    if (nextConfig.proxy.lanPort !== previousConfig.proxy.lanPort) {
      log('[config] proxy.lanPort changed; restart required for SIP socket rebinding');
    }
    if (nextConfig.proxy.webUiPort !== previousConfig.proxy.webUiPort) {
      log('[config] proxy.webUiPort changed; restart required for web UI rebinding');
    }

    const configured = await configureRuntime(nextConfig);
    if (configured) {
      log('[config] reloaded - proxy engine reconfigured');
    } else {
      log('[config] reload failed - proxy engine rejected config');
    }
  } catch (error: unknown) {
    log(`[config] reload failed: ${errorMessage(error)}`);
  }
}

async function updateConfig(updatesArg: any): Promise<IAppConfig> {
  const nextConfig = applyConfigUpdates(appConfig, updatesArg);
  await storage.writeAppConfig(nextConfig);
  await reloadConfig();
  return appConfig;
}

async function startProxyEngine(): Promise<void> {
  const started = await initProxyEngine(log);
  if (!started) {
    log('[FATAL] failed to start proxy engine');
    process.exit(1);
  }

  registerProxyEventHandlers({
    log,
    statusStore,
    faxBoxManager,
    faxJobManager,
    voiceboxManager,
    webRtcLinks,
    getBrowserDeviceIds: getAllBrowserDeviceIds,
    sendToBrowserDevice,
    broadcast: broadcastWs,
    onLinkWebRtcSession: requestWebRtcLink,
    onCloseWebRtcSession: (sessionId) => {
      void webrtcClose(sessionId);
    },
  });

  const configured = await configureRuntime(appConfig);
  if (!configured) {
    log('[FATAL] failed to configure proxy engine');
    process.exit(1);
  }

  const providerList = appConfig.providers.map((provider) => provider.displayName).join(', ');
  const deviceList = appConfig.devices.map((device) => device.displayName).join(', ');
  log(`proxy engine started | LAN ${appConfig.proxy.lanIp}:${appConfig.proxy.lanPort} | providers: ${providerList} | devices: ${deviceList}`);
}

async function main(): Promise<void> {
  await storage.init();
  appConfig = await storage.getAppConfig();

  statusStore = new StatusStore(appConfig);
  webRtcLinks = new WebRtcLinkManager();
  faxBoxManager = new FaxBoxManager(log, storage);
  faxJobManager = new FaxJobManager(log, storage);
  voiceboxManager = new VoiceboxManager(log, storage);

  await faxBoxManager.init(appConfig.faxboxes ?? []);
  await faxJobManager.init();
  await voiceboxManager.init(appConfig.voiceboxes ?? []);
  initWebRtcSignaling({ log });

  initWebUi({
    port: appConfig.proxy.webUiPort,
    getStatus,
    getConfig: () => appConfig,
    updateConfig,
    log,
    onStartCall: async (number, deviceId, providerId) => {
      log(`[dashboard] start call: ${number} device=${deviceId || 'any'} provider=${providerId || 'auto'}`);
      const callId = await makeCall(number, deviceId, providerId);
      if (!callId) {
        log(`[dashboard] call failed for ${number}`);
        return null;
      }
      log(`[dashboard] call started: ${callId}`);
      statusStore.noteDashboardCallStarted(callId, number, providerId);
      return { id: callId };
    },
    onHangupCall: (callId) => {
      void hangupCall(callId);
      return true;
    },
    faxBoxManager,
    faxJobManager,
    voiceboxManager,
    onWebRtcOffer: async (sessionId, sdp, ws) => {
      log(`[webrtc] offer from browser session=${sessionId.slice(0, 8)} sdp_type=${typeof sdp} sdp_len=${sdp?.length || 0}`);
      if (!sdp || typeof sdp !== 'string' || sdp.length < 10) {
        log(`[webrtc] WARNING: invalid SDP (type=${typeof sdp}), skipping offer`);
        return;
      }

      log(`[webrtc] sending offer to Rust (${sdp.length}b)...`);
      const result = await webrtcOffer(sessionId, sdp);
      log(`[webrtc] Rust result: ${JSON.stringify(result)?.slice(0, 200)}`);
      if (result?.sdp) {
        ws.send(JSON.stringify({ type: 'webrtc-answer', sessionId, sdp: result.sdp }));
        log(`[webrtc] answer sent to browser session=${sessionId.slice(0, 8)}`);
        return;
      }

      log('[webrtc] ERROR: no answer SDP from Rust');
    },
    onWebRtcIce: async (sessionId, candidate) => {
      await webrtcIce(sessionId, candidate as Parameters<typeof webrtcIce>[1]);
    },
    onWebRtcClose: async (sessionId) => {
      webRtcLinks.removeSession(sessionId);
      await webrtcClose(sessionId);
    },
    onWebRtcAccept: (callId, sessionId) => {
      log(`[webrtc] accept: callId=${callId} sessionId=${sessionId.slice(0, 8)}`);

      const pendingMedia = webRtcLinks.acceptCall(callId, sessionId);
      if (pendingMedia) {
        requestWebRtcLink(callId, sessionId, pendingMedia);
        return;
      }

      log(`[webrtc] session ${sessionId.slice(0, 8)} accepted, waiting for call_answered media info`);
    },
  });

  await startProxyEngine();
}

void main().catch((error) => {
  log(`[FATAL] ${errorMessage(error)}`);
  process.exit(1);
});

process.on('SIGINT', () => {
  log('SIGINT, exiting');
  shutdownProxyEngine();
  void storage.close();
  process.exit(0);
});

process.on('SIGTERM', () => {
  log('SIGTERM, exiting');
  shutdownProxyEngine();
  void storage.close();
  process.exit(0);
});
