export interface AppConfig {
  nodeEnv: 'development' | 'test' | 'production';
  port: number;
  logLevel: string;
  database: { url: string };
  redis: { url: string };
  jwt: {
    secret: string;
    refreshSecret: string;
    expiresIn: string;
    refreshExpiresIn: string;
  };
  ai: {
    /// DeepSeek 直连 API key(国内 endpoint,合规)。从 https://platform.deepseek.com/api_keys 取
    deepseekApiKey: string;
    /// DeepSeek base URL(默认官方;私有 / 镜像 endpoint 时覆盖)
    deepseekBaseUrl: string;
    /// 默认主力模型(deepseek-v4-pro / deepseek-v4-flash)
    defaultModel: string;
    /// Gemini API key(Google AI Studio;海外需可达 generativelanguage.googleapis.com)
    geminiApiKey: string;
    /// Gemini base URL(空=SDK 默认;走代理 / 镜像 endpoint 时覆盖)
    geminiBaseUrl: string;
    /// Gemini 默认模型(前端传逻辑键 "gemini" 时解析到此具体型号)
    geminiDefaultModel: string;
    /// DashScope(阿里云百炼)API key — Qwen-Omni-Realtime 实时教练用;只服务端,绝不下发浏览器
    dashscopeApiKey: string;
    /// Qwen Omni 实时模型名(实时坐席辅助教练)
    qwenOmniModel: string;
    /// Gemini Live 实时模型名(实时教练可选 provider)
    geminiLiveModel: string;
    /// Qwen(通义千问,DashScope OpenAI-兼容端点)— 话术生成可选 provider
    qwenApiKey: string;
    qwenBaseUrl: string;
    qwenDefaultModel: string;
    /// 单次 LLM 调用上限(秒),防卡死(安全网,默认 180;深度档每步各自计时)
    requestTimeoutSec: number;
    /**
     * 助手的**人设语气**（整块换掉系统提示词的 ③ 层，见 assistant-prompts 的 VOICE_DEFAULT）。
     * 空 = 用默认那套。宿主 / 品牌要自己的说话方式时配这个，⛔ 不要去改代码里的默认块。
     * ⚠️ 它只管**怎么说**：⛔ 不许写事实、工具名、界面部件名 —— 那些换语气的人不该有能力碰。
     */
    assistantVoice: string;
    /// 价格表(¥/M tokens)— 从 AI_PRICE_TABLE_JSON env 读;调价时改 env 重启即可
    priceTable: Record<string, { inHit: number; inMiss: number; out: number }>;
  };
  alert: { webhookUrl: string };
  cors: { origins: string[] };
  exchangeCodeTtlSeconds: number;
}

export function loadConfig(): AppConfig {
  return {
    nodeEnv: (process.env.NODE_ENV as AppConfig['nodeEnv']) ?? 'development',
    port: Number(process.env.PORT ?? 3001),
    logLevel: process.env.LOG_LEVEL ?? 'info',
    database: {
      url: required('DATABASE_URL'),
    },
    redis: {
      url: required('REDIS_URL'),
    },
    jwt: {
      secret: required('JWT_SECRET'),
      refreshSecret: required('JWT_REFRESH_SECRET'),
      expiresIn: process.env.JWT_EXPIRES_IN ?? '2h',
      refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
    },
    ai: {
      deepseekApiKey: process.env.DEEPSEEK_API_KEY ?? '',
      deepseekBaseUrl: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com',
      defaultModel: process.env.AI_DEFAULT_MODEL ?? 'deepseek-v4-flash',
      geminiApiKey: process.env.GEMINI_API_KEY ?? '',
      geminiBaseUrl: process.env.GEMINI_BASE_URL ?? '',
      geminiDefaultModel: process.env.GEMINI_DEFAULT_MODEL ?? 'gemini-3.5-flash',
      dashscopeApiKey: process.env.DASHSCOPE_API_KEY ?? '',
      qwenOmniModel: process.env.QWEN_OMNI_MODEL ?? 'qwen3-omni-flash-realtime',
      geminiLiveModel: process.env.GEMINI_LIVE_MODEL ?? 'gemini-3.1-flash-live-preview',
      qwenApiKey: process.env.QWEN_API_KEY ?? process.env.DASHSCOPE_API_KEY ?? '',
      qwenBaseUrl: process.env.QWEN_BASE_URL ?? 'https://dashscope.aliyuncs.com/compatible-mode/v1',
      qwenDefaultModel: process.env.QWEN_DEFAULT_MODEL ?? 'qwen3.8-max',
      // 单次 LLM 调用上限(秒)— 仅作"防永久挂起"安全网,不该卡正常请求。
      // 取 180s:给 pro(慢·精细)+ 深度档单步留足余量;真挂起 3 分钟后失败 → 走模板兜底。
      requestTimeoutSec: Number(process.env.AI_REQUEST_TIMEOUT_SEC ?? 180),
      assistantVoice: process.env.PAC_ASSISTANT_VOICE ?? '',
      priceTable: parsePriceTable(process.env.AI_PRICE_TABLE_JSON),
    },
    alert: {
      webhookUrl: process.env.ALERT_WEBHOOK_URL ?? '',
    },
    cors: {
      origins: (process.env.CORS_ORIGINS ?? '')
        .split(',')
        .map((s) => s.trim())
        .filter(Boolean),
    },
    exchangeCodeTtlSeconds: Number(process.env.EXCHANGE_CODE_TTL_SECONDS ?? 60),
  };
}

/**
 * AI 价格表 — 从 env JSON 读,失败兜底默认值。
 *
 * 单位:¥/M tokens(已按汇率换算,DeepSeek 公开价 USD * 7.2)。
 *   inHit:  输入命中 prompt cache 的部分(便宜 ~7x)
 *   inMiss: 输入未命中(全价)
 *   out:    输出(含 reasoning,DeepSeek 统一计费)
 *
 * 调价流程:
 *   1. 改 .env 里 AI_PRICE_TABLE_JSON
 *   2. 重启 service
 *   3. 历史 invocation 已落库 costYuan 不动(快照,不被新价污染)
 *   4. 新调用按新价计
 */
function parsePriceTable(raw: string | undefined): Record<string, { inHit: number; inMiss: number; out: number }> {
  // ⚠️ 价格核对于 2026-06(官方页):¥/M tokens。美元价按 ×7.2 折 RMB。
  //   用各家**标准价**(不含限时促销,促销到期不会少报);要按实付/促销算 → 用 AI_PRICE_TABLE_JSON 覆盖。
  //   DeepSeek:api-docs.deepseek.com/quick_start/pricing(V4-pro $0.003625/0.435/0.87、V4-flash $0.0028/0.14/0.28)
  //   Gemini 3.5 Flash:$0.15/1.5/9.0(cache/in/out)
  //   Qwen3.7-Max(百炼,RMB 直接):缓存输入 1.2 / 输入 12 / 输出 36;现有限时 5 折 = 6/18(促销价走 env)
  const DEFAULT = {
    'deepseek-v4-pro':   { inHit: 0.03, inMiss: 3.13, out: 6.26 },
    'deepseek-v4-flash': { inHit: 0.02, inMiss: 1.01, out: 2.02 },
    'gemini-3.5-flash':  { inHit: 1.08, inMiss: 10.8, out: 64.8 },
    'gemini-2.5-flash':  { inHit: 1.08, inMiss: 10.8, out: 64.8 },
    // Qwen3.7-Max 是旗舰最贵档(不是便宜模型):标准价 ¥/M 直接填
    'qwen3.7-max':       { inHit: 1.2,  inMiss: 12,   out: 36 },
    /**
     * 🔴 **qwen3.8-max 没有单价** —— 2026-08-13 换成默认模型时查过，
     *   公开渠道只查到"输出约 $6/M"这种口径不明的说法，⛔ 不据此编一个数填进来。
     * ⇒ 现在它会命中 estimateCostYuan 的兜底(并打一条 warn，见那里)。
     *   拿到控制台的真实单价后，⛔ 不用改代码 —— 设 env 即可：
     *     AI_PRICE_TABLE_JSON='{"qwen3.8-max":{"inHit":X,"inMiss":Y,"out":Z}}'
     */
  };
  if (!raw) return DEFAULT;
  try {
    const parsed = JSON.parse(raw) as Record<string, { inHit: number; inMiss: number; out: number }>;
    // 简易校验:确保每条都有 3 个 number 字段
    for (const [k, v] of Object.entries(parsed)) {
      if (typeof v.inHit !== 'number' || typeof v.inMiss !== 'number' || typeof v.out !== 'number') {
        throw new Error(`invalid entry for model "${k}"`);
      }
    }
    return parsed;
  } catch (err) {
    // eslint-disable-next-line no-console
    console.warn(`AI_PRICE_TABLE_JSON 解析失败,回落到默认值:`, err instanceof Error ? err.message : err);
    return DEFAULT;
  }
}

function required(name: string): string {
  const v = process.env[name];
  if (!v || v.trim() === '') {
    throw new Error(`Missing required env var: ${name}`);
  }
  return v;
}

// Optional: keep a no-op schema slot for ConfigModule API symmetry.
export const configValidationSchema = undefined;
