import { homedir } from 'node:os';
import { join } from 'node:path';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';

/** Deep merge source into target, preserving nested objects */
function deepMerge(target: Record<string, any>, source: Record<string, any>): void {
  for (const key of Object.keys(source)) {
    // Prevent prototype pollution
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
    if (
      source[key] !== null &&
      typeof source[key] === 'object' &&
      !Array.isArray(source[key]) &&
      typeof target[key] === 'object' &&
      target[key] !== null
    ) {
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

export interface AgenticMailConfig {
  stalwart: {
    url: string;
    adminUser: string;
    adminPassword: string;
  };
  smtp: {
    host: string;
    port: number;
  };
  imap: {
    host: string;
    port: number;
  };
  api: {
    port: number;
    host: string;
  };
  gateway?: {
    mode?: 'relay' | 'domain' | 'none';
    autoResume?: boolean;
  };
  sms?: {
    enabled?: boolean;
    phoneNumber?: string;
    forwardingEmail?: string;
    provider?: 'google_voice';
    configuredAt?: string;
  };
  /**
   * Operator's notification address. Used by the dispatcher's
   * bridge-escalation path: when sub-agents mail a bridge inbox AND
   * no fresh host session is available for a headless resume, the
   * API forwards a digest to this address so the operator gets a
   * phone push (via Gmail / Apple Mail / whichever app handles
   * their address). Set during install — the host agent (claude /
   * codex) collects it via the `setup_operator_email` MCP tool
   * during bootstrap and persists it here.
   *
   * Optional. When unset, escalations are still recorded as a
   * system event (visible in the web UI) but no email is sent.
   */
  operatorEmail?: string;
  /**
   * OpenAI API key — used only by the realtime voice bridge to open an
   * OpenAI Realtime (`gpt-realtime`) session for live phone calls. When
   * unset, phone missions still place and track calls (call-control),
   * but a 46elks realtime-media WebSocket cannot be bridged to a
   * conversational model. Read from the `OPENAI_API_KEY` env var or
   * `config.json`. Optional — no other feature depends on it.
   */
  openaiApiKey?: string;
  /**
   * v0.9.93 — voice-runtime provider keys. Each is a thin Bearer-token
   * for a provider's realtime API; required only when that provider
   * is selected as the runtime for a call. New providers register
   * themselves in `packages/core/src/phone/voice-providers/` and
   * declare which key field they consume — this record is the central
   * pool. Read from per-provider env vars (e.g. `XAI_API_KEY`) at boot.
   */
  voiceProviderKeys?: Record<string, string>;
  /**
   * v0.9.95 — install-wide default voice CHARACTER per provider
   * (e.g. `{ openai: 'cedar', grok: 'ara' }`). Optional — when a
   * provider's slot is empty, the bridge falls back to the
   * provider's own `defaultVoice`. Per-agent persona + per-call
   * mission policy still beat this. The CLI's `agenticmail
   * setup-voice` voice picker writes here.
   */
  voiceProviderVoices?: Record<string, string>;
  /**
   * v0.9.93 — default voice-runtime provider id for phone missions
   * that don't pin one on their own policy. `'openai'` (the existing
   * default) or any provider registered in `voice-providers/`.
   * Read from `AGENTICMAIL_VOICE_RUNTIME` env var or `config.json`.
   */
  voiceRuntime?: string;
  masterKey: string;
  dataDir: string;
}

const DEFAULT_CONFIG: AgenticMailConfig = {
  stalwart: {
    url: 'http://localhost:8080',
    adminUser: 'admin',
    adminPassword: 'changeme',
  },
  smtp: {
    host: 'localhost',
    port: 587,
  },
  imap: {
    host: 'localhost',
    port: 143,
  },
  api: {
    // Default API port: 3829.
    //
    // We deliberately avoid 3000 (React/Express default), 3100 (Grafana
    // Loki, also a common Express convention), 3200 (Grafana Tempo),
    // 3300 (LMS-style apps), 4000/5000/8000/8080 (too common). 3829 sits
    // in a quiet stretch — unassigned by IANA, and far enough from
    // common dev tooling that a fresh `agenticmail setup` rarely
    // collides. Users can override via the `AGENTICMAIL_API_PORT` env
    // var or by editing `~/.agenticmail/config.json` after install.
    port: 3829,
    host: '127.0.0.1',
  },
  masterKey: '',
  dataDir: join(homedir(), '.agenticmail'),
};

export function resolveConfig(overrides?: Partial<AgenticMailConfig>): AgenticMailConfig {
  const env = process.env;
  const config: AgenticMailConfig = {
    stalwart: {
      url: env.STALWART_URL ?? DEFAULT_CONFIG.stalwart.url,
      adminUser: env.STALWART_ADMIN_USER ?? DEFAULT_CONFIG.stalwart.adminUser,
      adminPassword: env.STALWART_ADMIN_PASSWORD ?? DEFAULT_CONFIG.stalwart.adminPassword,
    },
    smtp: {
      host: env.SMTP_HOST ?? DEFAULT_CONFIG.smtp.host,
      port: (() => { const p = parseInt(env.SMTP_PORT ?? ''); return isNaN(p) ? DEFAULT_CONFIG.smtp.port : p; })(),
    },
    imap: {
      host: env.IMAP_HOST ?? DEFAULT_CONFIG.imap.host,
      port: (() => { const p = parseInt(env.IMAP_PORT ?? ''); return isNaN(p) ? DEFAULT_CONFIG.imap.port : p; })(),
    },
    api: {
      port: (() => { const p = parseInt(env.AGENTICMAIL_API_PORT ?? ''); return isNaN(p) ? DEFAULT_CONFIG.api.port : p; })(),
      host: env.AGENTICMAIL_API_HOST ?? DEFAULT_CONFIG.api.host,
    },
    masterKey: env.AGENTICMAIL_MASTER_KEY ?? DEFAULT_CONFIG.masterKey,
    dataDir: env.AGENTICMAIL_DATA_DIR?.replace(/^~(?=\/|$)/, homedir()) ?? DEFAULT_CONFIG.dataDir,
  };

  // OpenAI key for the realtime voice bridge — env-only by default; a
  // value in config.json (merged below) takes precedence if present.
  if (env.OPENAI_API_KEY) config.openaiApiKey = env.OPENAI_API_KEY;

  // v0.9.93 — voice-provider keys. The bridge looks up providers
  // through packages/core/src/phone/voice-providers/; each provider
  // declares its env var name. Collecting them all here means the
  // registry just reads `config.voiceProviderKeys[<id>]` later.
  // Hard-coded for now; future improvement is to enumerate
  // listVoiceProviders() here, but that would create an import cycle
  // (config → voice-providers → registry → … config) so we keep the
  // list literal.
  if (env.XAI_API_KEY) {
    config.voiceProviderKeys = config.voiceProviderKeys ?? {};
    config.voiceProviderKeys.grok = env.XAI_API_KEY;
  }
  if (env.AGENTICMAIL_VOICE_RUNTIME && env.AGENTICMAIL_VOICE_RUNTIME.trim()) {
    config.voiceRuntime = env.AGENTICMAIL_VOICE_RUNTIME.trim();
  }

  // Merge file-based config if it exists (deep merge to preserve nested objects)
  const configPath = join(config.dataDir, 'config.json');
  if (existsSync(configPath)) {
    try {
      const fileConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
      deepMerge(config, fileConfig);
    } catch {
      console.warn('[agenticmail] Ignoring malformed config file:', configPath);
    }
  }

  // Apply explicit overrides last (deep merge)
  if (overrides) {
    deepMerge(config, overrides);
  }

  return config;
}

export function ensureDataDir(config: AgenticMailConfig): void {
  if (!existsSync(config.dataDir)) {
    mkdirSync(config.dataDir, { recursive: true });
  }
}

export function saveConfig(config: AgenticMailConfig): void {
  ensureDataDir(config);
  const configPath = join(config.dataDir, 'config.json');
  writeFileSync(configPath, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
}
