/**
 * Application configuration models and normalization helpers.
 *
 * All network addresses, credentials, provider settings, device definitions,
 * and routing rules are persisted through SmartData.
 */

import type { IFaxBoxConfig } from './faxbox.ts';
import type { IVoiceboxConfig } from './voicebox.js';

// ---------------------------------------------------------------------------
// Shared types (previously in ts/sip/types.ts, now inlined)
// ---------------------------------------------------------------------------

export interface IEndpoint {
  address: string;
  port: number;
}

// ---------------------------------------------------------------------------
// Config interfaces
// ---------------------------------------------------------------------------

export interface IQuirks {
  earlyMediaSilence: boolean;
  silencePayloadType?: number;
  silenceMaxPackets?: number;
}

export interface IProviderConfig {
  id: string;
  displayName: string;
  domain: string;
  outboundProxy: IEndpoint;
  username: string;
  password: string;
  registerIntervalSec: number;
  codecs: number[];
  quirks: IQuirks;
}

export interface IDeviceConfig {
  id: string;
  displayName: string;
  expectedAddress: string;
  extension: string;
}

export type TIncomingNumberMode = 'single' | 'range' | 'regex';

export interface IIncomingNumberConfig {
  id: string;
  label: string;
  providerId?: string;
  mode: TIncomingNumberMode;
  countryCode?: string;
  areaCode?: string;
  localNumber?: string;
  rangeEnd?: string;
  pattern?: string;

  // Legacy persisted fields kept for migration compatibility.
  number?: string;
  rangeStart?: string;
}

// ---------------------------------------------------------------------------
// Match/Action routing model
// ---------------------------------------------------------------------------

/**
 * Criteria for matching a call to a route.
 * All specified fields must match (AND logic).
 * Omitted fields are wildcards (match anything).
 */
export interface ISipRouteMatch {
  /** Whether this route applies to inbound or outbound calls. */
  direction: 'inbound' | 'outbound';

  /**
   * Match the normalized called number.
   *
   * Inbound: matches the provider-delivered DID / Request-URI user part.
   * Outbound: matches the normalized dialed digits.
   * Supports: exact string, numeric range `start..end`, prefix with trailing '*' (e.g. "+4930*"), or regex ("/^\\+49/").
   */
  numberPattern?: string;

  /** Match caller ID / From number (same pattern syntax). */
  callerPattern?: string;

  /** For inbound: match by source provider ID. */
  sourceProvider?: string;

  /** For outbound: match by source device ID (or 'browser' for browser devices). */
  sourceDevice?: string;
}

/** What to do when a route matches. */
export interface ISipRouteAction {
  // --- Inbound actions (ring targets) ---

  /** Device IDs to ring. Empty/omitted = ring all devices. */
  targets?: string[];

  /** Also ring connected browser clients. Default false. */
  ringBrowsers?: boolean;

  // --- Inbound actions (IVR / voicemail) ---

  /** Voicemail fallback for matched inbound routes. */
  voicemailBox?: string;

  /** Fax inbox target for matched inbound routes. */
  faxBox?: string;

  /** Route to an IVR menu by menu ID (skip ringing devices). */
  ivrMenuId?: string;

  /** Reserved for future no-answer handling. */
  noAnswerTimeout?: number;

  // --- Outbound actions (provider selection) ---

  /** Provider ID to use for outbound. */
  provider?: string;

  /** Failover provider IDs, tried in order if primary is unregistered. */
  failoverProviders?: string[];

  // --- Number transformation (outbound) ---

  /** Strip this prefix from the dialed number before sending to provider. */
  stripPrefix?: string;

  /** Prepend this prefix to the dialed number before sending to provider. */
  prependPrefix?: string;
}

/** A single routing rule — the core unit of the match/action model. */
export interface ISipRoute {
  /** Unique identifier for this route. */
  id: string;

  /** Human-readable name shown in the UI. */
  name: string;

  /** Higher priority routes are evaluated first. Default 0. */
  priority: number;

  /** Disabled routes are skipped during evaluation. Default true. */
  enabled: boolean;

  /** Criteria that a call must match for this route to apply. */
  match: ISipRouteMatch;

  /** What to do when the route matches. */
  action: ISipRouteAction;
}

export interface IRoutingConfig {
  routes: ISipRoute[];
}

export interface IProxyConfig {
  lanIp: string;
  lanPort: number;
  publicIpSeed: string | null;
  rtpPortRange: { min: number; max: number };
  webUiPort: number;
}

export interface IContact {
  id: string;
  name: string;
  number: string;
  company?: string;
  notes?: string;
  starred?: boolean;
}

// ---------------------------------------------------------------------------
// Voicebox configuration
// ---------------------------------------------------------------------------

// Canonical definition lives in voicebox.ts (imported at the top of this
// file) — re-exported here so consumers can import everything from a
// single config module without pulling in the voicebox implementation.
// This used to be a duplicated interface and caused
// "number | undefined is not assignable to number" type errors when
// passing config.voiceboxes into VoiceboxManager.init().
export type { IVoiceboxConfig };
export type { IFaxBoxConfig };

// ---------------------------------------------------------------------------
// IVR configuration
// ---------------------------------------------------------------------------

/** An action triggered by a digit press in an IVR menu. */
export type TIvrAction =
  | { type: 'route-extension'; extensionId: string }
  | { type: 'route-voicemail'; boxId: string }
  | { type: 'submenu'; menuId: string }
  | { type: 'play-message'; promptId: string }
  | { type: 'transfer'; number: string; providerId?: string }
  | { type: 'repeat' }
  | { type: 'hangup' };

/** A single digit→action mapping in an IVR menu. */
export interface IIvrMenuEntry {
  /** Digit: '0'-'9', '*', '#'. */
  digit: string;
  /** Action to take when this digit is pressed. */
  action: TIvrAction;
}

/** An IVR menu with a prompt and digit mappings. */
export interface IIvrMenu {
  /** Unique menu ID. */
  id: string;
  /** Human-readable name. */
  name: string;
  /** TTS text for the menu prompt. */
  promptText: string;
  /** TTS voice ID for the prompt. */
  promptVoice?: string;
  /** Digit→action entries. */
  entries: IIvrMenuEntry[];
  /** Seconds to wait for a digit after prompt finishes (default 5). */
  timeoutSec?: number;
  /** Maximum retries before executing timeout action (default 3). */
  maxRetries?: number;
  /** Action on timeout (no digit pressed). */
  timeoutAction: TIvrAction;
  /** Action on invalid digit. */
  invalidAction: TIvrAction;
}

/** Top-level IVR configuration. */
export interface IIvrConfig {
  /** Whether the IVR system is active. */
  enabled: boolean;
  /** IVR menu definitions. */
  menus: IIvrMenu[];
  /** The menu to start with for incoming calls. */
  entryMenuId: string;
}

// ---------------------------------------------------------------------------
// App config
// ---------------------------------------------------------------------------

export interface IAppConfig {
  proxy: IProxyConfig;
  providers: IProviderConfig[];
  devices: IDeviceConfig[];
  incomingNumbers?: IIncomingNumberConfig[];
  routing: IRoutingConfig;
  contacts: IContact[];
  faxboxes?: IFaxBoxConfig[];
  voiceboxes?: IVoiceboxConfig[];
  ivr?: IIvrConfig;
}

// ---------------------------------------------------------------------------
// Defaults and normalization
// ---------------------------------------------------------------------------

function requiredInitialEnv(keyArg: string): string {
  const value = process.env[keyArg];
  if (!value) {
    throw new Error(`Missing required initial config environment variable: ${keyArg}`);
  }
  return value;
}

function numberFromEnv(keyArg: string, fallbackArg: number): number {
  const value = process.env[keyArg];
  if (!value) return fallbackArg;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallbackArg;
}

export function normalizeConfig(cfg: IAppConfig): IAppConfig {
  try {
    // Basic validation.
    if (!cfg.proxy) throw new Error('config: missing "proxy" section');
    if (!cfg.proxy.lanIp) throw new Error('config: missing proxy.lanIp');
    if (!cfg.proxy.lanPort) throw new Error('config: missing proxy.lanPort');
    if (!cfg.proxy.rtpPortRange?.min || !cfg.proxy.rtpPortRange?.max) {
      throw new Error('config: missing proxy.rtpPortRange.min/max');
    }
    cfg.proxy.webUiPort ??= 3060;
    cfg.proxy.publicIpSeed ??= null;

    cfg.providers ??= [];
    for (const p of cfg.providers) {
      if (!p.id || !p.domain || !p.outboundProxy || !p.username || !p.password) {
        throw new Error(`config: provider "${p.id || '?'}" missing required fields`);
      }
      p.displayName ??= p.id;
      p.registerIntervalSec ??= 300;
      p.codecs ??= [9, 0, 8, 101];
      p.quirks ??= { earlyMediaSilence: false };
    }

    if (!Array.isArray(cfg.devices) || !cfg.devices.length) {
      throw new Error('config: need at least one device');
    }
    for (const d of cfg.devices) {
      if (!d.id || !d.expectedAddress) {
        throw new Error(`config: device "${d.id || '?'}" missing required fields`);
      }
      d.displayName ??= d.id;
      d.extension ??= '100';
    }

    cfg.incomingNumbers ??= [];
    for (const incoming of cfg.incomingNumbers) {
      if (!incoming.id) incoming.id = `incoming-${Date.now()}`;
      incoming.label ??= incoming.id;
      incoming.mode ??= incoming.pattern ? 'regex' : incoming.rangeStart || incoming.rangeEnd ? 'range' : 'single';
      incoming.countryCode ??= incoming.mode === 'regex' ? undefined : '+49';
    }

    cfg.routing ??= { routes: [] };
    cfg.routing.routes ??= [];

    cfg.contacts ??= [];
    for (const c of cfg.contacts) {
      c.starred ??= false;
    }

    cfg.faxboxes ??= [];
    for (const fb of cfg.faxboxes) {
      fb.enabled ??= true;
      fb.maxMessages ??= 50;
    }

    cfg.voiceboxes ??= [];
    for (const vb of cfg.voiceboxes) {
      vb.enabled ??= true;
      vb.noAnswerTimeoutSec ??= 25;
      vb.maxRecordingSec ??= 120;
      vb.maxMessages ??= 50;
      vb.greetingVoice ??= 'af_bella';
    }

    if (cfg.ivr) {
      cfg.ivr.enabled ??= false;
      cfg.ivr.menus ??= [];
      for (const menu of cfg.ivr.menus) {
        menu.timeoutSec ??= 5;
        menu.maxRetries ??= 3;
        menu.entries ??= [];
      }
    }

    return cfg;
  } catch (error) {
    throw error;
  }
}

export function createInitialConfigFromEnv(): IAppConfig {
  return normalizeConfig({
    proxy: {
      lanIp: requiredInitialEnv('SIPROUTER_LAN_IP'),
      lanPort: numberFromEnv('SIPROUTER_LAN_PORT', 5070),
      publicIpSeed: process.env.SIPROUTER_PUBLIC_IP || null,
      rtpPortRange: {
        min: numberFromEnv('SIPROUTER_RTP_PORT_MIN', 20000),
        max: numberFromEnv('SIPROUTER_RTP_PORT_MAX', 20200),
      },
      webUiPort: numberFromEnv('SIPROUTER_WEB_UI_PORT', 3060),
    },
    providers: [],
    devices: [
      {
        id: process.env.SIPROUTER_INITIAL_DEVICE_ID || 'desk-phone',
        displayName: process.env.SIPROUTER_INITIAL_DEVICE_DISPLAY_NAME || 'Desk Phone',
        expectedAddress: requiredInitialEnv('SIPROUTER_INITIAL_DEVICE_ADDRESS'),
        extension: process.env.SIPROUTER_INITIAL_DEVICE_EXTENSION || '100',
      },
    ],
    incomingNumbers: [],
    routing: { routes: [] },
    contacts: [],
    faxboxes: [],
    voiceboxes: [],
    ivr: {
      enabled: false,
      entryMenuId: 'main-menu',
      menus: [],
    },
  });
}

export function maskConfig(configArg: IAppConfig): IAppConfig {
  return {
    ...configArg,
    providers: configArg.providers?.map((providerArg) => ({
      ...providerArg,
      password: providerArg.password ? '••••••' : providerArg.password,
    })) || [],
  };
}

export function applyConfigUpdates(configArg: IAppConfig, updatesArg: any): IAppConfig {
  const cfg = JSON.parse(JSON.stringify(configArg)) as IAppConfig;

  if (updatesArg.providers) {
    for (const up of updatesArg.providers) {
      const existing = cfg.providers?.find((p: any) => p.id === up.id);
      if (existing) {
        if (up.displayName !== undefined) existing.displayName = up.displayName;
        if (up.password && up.password !== '••••••') existing.password = up.password;
        if (up.domain !== undefined) existing.domain = up.domain;
        if (up.outboundProxy !== undefined) existing.outboundProxy = up.outboundProxy;
        if (up.username !== undefined) existing.username = up.username;
        if (up.registerIntervalSec !== undefined) existing.registerIntervalSec = up.registerIntervalSec;
        if (up.codecs !== undefined) existing.codecs = up.codecs;
        if (up.quirks !== undefined) existing.quirks = up.quirks;
      }
    }
  }

  if (updatesArg.addProvider) {
    cfg.providers ??= [];
    cfg.providers.push(updatesArg.addProvider);
  }

  if (updatesArg.removeProvider) {
    cfg.providers = (cfg.providers || []).filter((p: any) => p.id !== updatesArg.removeProvider);
    if (cfg.routing?.routes) {
      cfg.routing.routes = cfg.routing.routes.filter((r: any) =>
        r.match?.sourceProvider !== updatesArg.removeProvider &&
        r.action?.provider !== updatesArg.removeProvider
      );
    }
  }

  if (updatesArg.devices) {
    for (const ud of updatesArg.devices) {
      const existing = cfg.devices?.find((d: any) => d.id === ud.id);
      if (existing && ud.displayName !== undefined) existing.displayName = ud.displayName;
    }
  }
  if (updatesArg.incomingNumbers !== undefined) cfg.incomingNumbers = updatesArg.incomingNumbers;
  if (updatesArg.routing?.routes) cfg.routing.routes = updatesArg.routing.routes;
  if (updatesArg.contacts !== undefined) cfg.contacts = updatesArg.contacts;
  if (updatesArg.faxboxes !== undefined) cfg.faxboxes = updatesArg.faxboxes;
  if (updatesArg.voiceboxes !== undefined) cfg.voiceboxes = updatesArg.voiceboxes;
  if (updatesArg.ivr !== undefined) cfg.ivr = updatesArg.ivr;

  return normalizeConfig(cfg);
}

// Route resolution, pattern matching, and provider/device lookup
// are now handled entirely by the Rust proxy-engine.
