#!/usr/bin/env node
import {
  ConfigError,
  EnvLoader
} from "./chunk-TZQHYZKT.js";
import {
  CONFIG_DIR_NAME,
  CONFIG_FILE_NAME,
  HISTORY_DIR_NAME,
  PLUGINS_DIR_NAME
} from "./chunk-KRU4DFRH.js";

// src/config/config-manager.ts
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { join } from "path";
import { homedir } from "os";

// src/config/schema.ts
import { z } from "zod";
var CustomModelSchema = z.object({
  id: z.string(),
  displayName: z.string().optional(),
  contextWindow: z.number().optional()
});
var CustomProviderConfigSchema = z.object({
  id: z.string(),
  // 唯一 ID，不能与内置 provider 重名
  displayName: z.string(),
  // 显示名称
  apiKey: z.string().optional(),
  // 可选：直接在此写 key（也可通过 apiKeys 字段或环境变量提供）
  baseUrl: z.string(),
  // OpenAI 兼容 API 的 base URL（必填）
  defaultModel: z.string(),
  // 默认使用的模型 ID
  models: z.array(CustomModelSchema).default([]),
  timeout: z.number().optional()
  // 请求超时（ms），覆盖全局默认值
});
var ModelParamsSchema = z.object({
  temperature: z.number().min(0).max(2).optional(),
  maxTokens: z.number().int().positive().optional(),
  timeout: z.number().int().positive().optional(),
  /** 是否启用深度思考（thinking）模式，Claude Sonnet/Opus、GLM-5 等 */
  thinking: z.boolean().optional(),
  /** thinking 模式的 token 预算（最小 1024，仅 Claude Extended Thinking 使用） */
  thinkingBudget: z.number().int().min(1024).optional()
});
var UserProfileSchema = z.object({
  /** 真实姓名（如 "Jin Zhengdong"） */
  name: z.string().optional(),
  /** 昵称/称呼偏好（如 "东叔"），AI 会以此称呼你 */
  nickname: z.string().optional(),
  /** 职业角色（如 "Full-stack developer"、"Data scientist"） */
  role: z.string().optional(),
  /** 个人简介 / 专长描述（1-3 句话） */
  bio: z.string().optional(),
  /** 兴趣领域或技术栈（如 ["TypeScript", "Rust", "AI/ML"]） */
  interests: z.array(z.string()).default([]),
  /** 语言偏好（如 "zh-CN"、"en"），AI 将据此选择交流语言 */
  locale: z.string().optional(),
  /** 自定义人设补充（自由格式，直接注入 system prompt） */
  extra: z.string().optional()
}).default({});
var ConfigSchema = z.object({
  version: z.string().default("1.0.0"),
  // 用户身份档案 — 跨 Provider 的 "灵魂"
  userProfile: UserProfileSchema,
  defaultProvider: z.string().default("claude"),
  // 每个 provider 的默认模型（key 为 provider ID）
  defaultModels: z.record(z.string()).default({}),
  // API Keys：放宽为任意 record，支持自定义 provider ID
  apiKeys: z.record(z.string()).default({}),
  customBaseUrls: z.record(z.string()).default({}),
  // Per-provider timeout in ms (e.g. { deepseek: 60000 })
  // ⚠️  Timeout 优先级说明（L5）：
  //   1. modelParams[modelId].timeout  — 最高优先级，精确到具体模型（如 deepseek-reasoner 需要更长超时）
  //   2. timeouts[providerId]          — Provider 级默认，覆盖内置默认值
  //   3. 内置 Provider 的硬编码默认值  — 最低兜底（如 OpenAI 兼容系列默认 60000ms）
  //   实现位置：src/providers/registry.ts initialize()，将 timeouts[id] 注入 provider
  timeouts: z.record(z.number()).default({}),
  // HTTP/HTTPS 代理地址（Node.js 不自动使用系统代理，需显式配置）
  // 例：http://127.0.0.1:10809
  // 也可通过环境变量 HTTPS_PROXY / HTTP_PROXY 覆盖
  proxy: z.string().optional(),
  // 自定义 Provider 列表（OpenAI 兼容接口，无需改代码）
  customProviders: z.array(CustomProviderConfigSchema).default([]),
  // 按模型 ID 存储的推理参数（key 为模型 ID，如 "deepseek-chat"）
  modelParams: z.record(ModelParamsSchema).default({}),
  ui: z.object({
    streaming: z.boolean().default(true),
    markdownRendering: z.boolean().default(true),
    showTokenCount: z.boolean().default(true),
    /** 桌面通知阈值（毫秒）：AI 任务耗时超过此值时发送系统通知。0 = 禁用。默认 10000 (10s) */
    notificationThreshold: z.number().default(1e4),
    /** 终端输出折行宽度。0 = 自动（使用终端宽度），>0 = 固定列宽。默认 0 */
    wordWrap: z.number().int().min(0).default(0),
    /** Skill 文件过大警告阈值（字符数）。超过此值的 skill 在启动加载时打印警告。0 = 静默。默认 10000 */
    skillSizeWarn: z.number().int().min(0).default(1e4),
    /** 颜色主题：'dark'（默认）| 'light' | 'custom' */
    theme: z.enum(["dark", "light", "custom"]).default("dark"),
    /** 自定义颜色覆盖（仅在 theme='custom' 时生效）。值为 chalk 颜色名或 '#hex'，支持 'bold.cyan' 组合 */
    colors: z.object({
      prompt: z.string().optional(),
      info: z.string().optional(),
      warning: z.string().optional(),
      error: z.string().optional(),
      success: z.string().optional(),
      dim: z.string().optional(),
      accent: z.string().optional(),
      toolCall: z.string().optional(),
      toolResult: z.string().optional(),
      heading: z.string().optional()
    }).optional()
  }).default({}),
  session: z.object({
    autoSave: z.boolean().default(true),
    maxHistoryDays: z.number().default(30),
    systemPrompt: z.string().optional()
  }).default({}),
  // 项目上下文文件配置
  // 启动时自动读取并注入 system prompt，类似 Claude Code 的 CLAUDE.md 机制
  // 默认按顺序查找：AICLI.md → CLAUDE.md → .aicli/context.md
  // 设为 false 可禁用此功能
  contextFile: z.union([z.string(), z.literal(false)]).default("auto"),
  // Google Custom Search API 的 Search Engine ID (cx 参数)
  // API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
  // CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
  googleSearchEngineId: z.string().optional(),
  // google_search 工具的安全开关（v0.4.135+）
  // 默认 disabled。这个工具调用 Google Custom Search API，没有原生 daily-spend cap——
  // 业内有多起 key 被盗后产生数万美元账单的真实案例。现在内置 web_search 工具走 Bing/Google
  // 公开搜索页 scraping，零 key 零账单，是大多数场景下的默认选择。
  // 仍想用 API 版？把 enabled 设为 true，并务必到 GCP Console 把 Custom Search Quota 设硬上限。
  googleSearch: z.object({
    enabled: z.boolean().default(false)
  }).default({ enabled: false }),
  // MCP (Model Context Protocol) 全局开关（v0.4.135+）
  // false 时启动跳过 connectAll()，所有 MCP 服务器都不连，节省冷启动时间 + 工具定义 token。
  // 对应你的 skills（whitelist 不依赖 MCP 工具时）完全不受影响。
  mcpEnabled: z.boolean().default(true),
  // MCP (Model Context Protocol) 服务器配置
  // 声明外部 MCP 服务器，启动时自动连接、发现工具并注册
  // 配置格式兼容 Claude Desktop（command + args + env）
  // 示例：{ "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"] } }
  mcpServers: z.record(z.object({
    command: z.string(),
    args: z.array(z.string()).default([]),
    env: z.record(z.string()).optional(),
    timeout: z.number().default(3e4)
  })).default({}),
  // 工具执行钩子（shell 命令，模板变量：{tool} {dangerLevel} {args} {status}）
  hooks: z.object({
    preToolExecution: z.string().optional(),
    postToolExecution: z.string().optional()
  }).optional(),
  // 工具权限规则（按顺序匹配第一个生效）
  permissionRules: z.array(z.object({
    tool: z.string(),
    action: z.enum(["auto-approve", "deny", "confirm"]),
    when: z.object({
      dangerLevel: z.enum(["safe", "write", "destructive"]).optional(),
      pathPattern: z.string().optional()
    }).optional()
  })).default([]),
  // 无规则匹配时的默认权限动作
  defaultPermission: z.enum(["auto-approve", "deny", "confirm"]).default("confirm"),
  // 自动上下文压缩开关
  // 当对话估算 token 数超过模型 contextWindow 的 80% 时，自动触发 compact 压缩旧消息
  // 默认开启。设为 false 则仅在手动 /compact 时压缩
  autoCompact: z.boolean().default(true),
  // YOLO 模式持久开关（v0.4.126+）
  // true 时 REPL 启动即开启 sessionAutoApprove，等同于每次开 session 都自动跑 `/yolo`。
  // 所有 write / destructive 工具都会跳过确认 —— 仅在完全信任自己当前工作目录时启用。
  // 启动 banner 会显示醒目提示，避免"忘了配置开着"的事故。
  alwaysYolo: z.boolean().default(false),
  // Agentic 工具调用循环单次对话最大轮次（默认 200）。
  // 超过此值后 AI 被强制停止调用工具并生成总结。
  // 建议范围：25（保守）~ 1000（宽松，接近 Claude Code）。
  // CLI `--max-tool-rounds <n>` 可覆盖此值。
  maxToolRounds: z.number().int().min(1).max(1e4).default(200),
  // Auto-pause checkpoint：Agentic 循环每隔多少轮暂停一次，让用户确认方向或中途介入。
  // 默认 50；设为 0 禁用（完全自动执行到完成或 maxToolRounds 用尽）。
  // CLI 与 Web UI 行为一致：CLI 用 readline question 提示，Web UI 弹出对话框。
  autoPauseInterval: z.number().int().min(0).max(1e4).default(50),
  // 单次工具输出（如 read_file、bash、grep_files）返回给 AI 的最大字符数上限。
  // 默认 500_000 (~500K chars ≈ 6000-8000 行代码)。
  // 实际上限还会受模型 contextWindow 动态约束（取 contextWindow/4 作为下限）。
  // 设置为 0 或未配置时使用默认值；不建议设为小于 12_000 或大于模型 contextWindow/2。
  maxToolOutputChars: z.number().int().min(0).default(5e5),
  // 月度成本预算（USD）。
  // 设置后，每次 AI 回复后会跟踪成本，接近或超过预算时在 /status 和 /cost 中显示警告。
  // 默认 0 = 不限制。例：50 表示每月最多花 $50。
  monthlyBudget: z.number().min(0).default(0),
  // 插件加载开关（安全控制）
  // 默认 false：不自动加载 ~/.aicli/plugins/ 中的插件文件。
  // 插件以完整 Node.js 权限在主进程中执行（可读写文件、访问网络、执行命令），
  // 必须确认插件来源可信后，再设为 true 启用。
  // 可通过 /config 命令或直接编辑 ~/.aicli/config.json 开启。
  allowPlugins: z.boolean().default(false),
  // 敏感信息脱敏（v0.4.88+，2026-04 凭据泄漏事件后引入）
  // 会话保存到磁盘 / 发送给 Provider 前，自动将 API key、密码、PEM 私钥等
  // 按正则替换为 [REDACTED:kind] 占位符。模式定义在 src/security/redactor.ts。
  //
  // redactOnSave 默认 true：保存到 ~/.aicli/history/*.json 时脱敏（推荐开启）
  // redactOnSend 默认 false：发送到 Provider 时脱敏（激进，可能影响工具结果）
  // mode:
  //   'default' — 使用内置 13 条模式（推荐）
  //   'strict'  — 同 default，但阈值更低（更激进，可能误报）
  //   'off'     — 禁用所有模式（等同 redactOnSave=false）
  // customPatterns — 用户补充正则字符串（支持 /pattern/flags 形式），
  //                  无效正则会静默跳过，不会中断保存。
  security: z.object({
    redactOnSave: z.boolean().default(true),
    redactOnSend: z.boolean().default(false),
    mode: z.enum(["default", "strict", "off"]).default("default"),
    customPatterns: z.array(z.string()).default([])
  }).default({}),
  // 智能模型路由（v0.4.68+）
  // 按用户每轮输入的内容/标签/长度动态选择模型，在同一 provider 内切换，
  // 例：短问题走 haiku（省钱），planning 走 opus（质量）。
  // enabled=false 时永远返回当前模型。rules 按顺序匹配，首个命中的规则生效。
  // 每个 rule 的 match 必须至少有一个条件（tag/contains/maxLength/minLength）。
  // 详见 src/core/model-router.ts。
  routing: z.object({
    enabled: z.boolean().default(false),
    rules: z.array(z.object({
      match: z.object({
        contains: z.array(z.string()).optional(),
        maxLength: z.number().int().positive().optional(),
        minLength: z.number().int().positive().optional(),
        tag: z.string().optional()
      }),
      model: z.string(),
      name: z.string().optional()
    })).default([]),
    fallback: z.string().optional()
  }).default({ enabled: false, rules: [] }),
  // Provider 容错（v0.4.144+）：网络抖动 / 5xx / 429 时，先在当前 provider 上做指数退避重试，
  // 仍失败则按顺序尝试 chain 里的 fallback provider。auth / 400 / 422 等"硬失败"直接跳过同
  // provider 重试，但仍会尝试 chain（不同 key/schema 可能能成功）。
  // 关键约束：流式 API 一旦已经向终端 yield 过 chunk，错误立刻向上抛 —— 不能"已经吐了一半"再
  // 重新发起请求，会撕裂用户看到的输出。
  // enabled 默认 false：保持向前兼容，未配置 chain 的用户行为不变。
  fallback: z.object({
    enabled: z.boolean().default(false),
    /** 同 provider 重试次数（不含首次调用）。0 = 不重试，直接走 chain。 */
    retries: z.number().int().min(0).max(10).default(2),
    /** 首次退避等待（ms），后续按 2 的幂指数增长。 */
    initialBackoffMs: z.number().int().positive().default(500),
    /** 单次退避的硬上限（ms）。 */
    maxBackoffMs: z.number().int().positive().default(8e3),
    /** 兜底 provider 链，每个 entry 仅尝试 1 次。model 省略则使用 provider 的 defaultModel。 */
    chain: z.array(z.object({
      provider: z.string(),
      model: z.string().optional()
    })).default([])
  }).default({ enabled: false, retries: 2, initialBackoffMs: 500, maxBackoffMs: 8e3, chain: [] })
});

// src/config/config-manager.ts
var ConfigManager = class {
  configDir;
  configPath;
  config;
  constructor(configDir) {
    this.configDir = configDir ?? join(homedir(), CONFIG_DIR_NAME);
    this.configPath = join(this.configDir, CONFIG_FILE_NAME);
    this.config = this.load();
    if (this.migrateLegacyModels()) {
      this.save();
    }
  }
  load() {
    if (!existsSync(this.configPath)) {
      return ConfigSchema.parse({});
    }
    try {
      const raw = JSON.parse(readFileSync(this.configPath, "utf-8"));
      return ConfigSchema.parse(raw);
    } catch (err) {
      throw new ConfigError(
        `Config file at ${this.configPath} is invalid. Delete it and run 'ai-cli config' to recreate.
${err}`
      );
    }
  }
  /**
   * One-time auto-heal for retired model IDs (v0.4.141+).
   *
   * v0.4.140 dropped `deepseek-chat` / `deepseek-reasoner` from the provider
   * model list but didn't migrate `~/.aicli/config.json`. Users who had ever
   * called `/model` to pin one of these legacy ids would land in a broken
   * state on next startup (banner shows `Model: deepseek-chat`, but `/model`
   * picker no longer lists it).
   *
   * This pass is idempotent — if nothing matches, returns false and no save
   * happens. Safe to run on every load.
   *
   * @returns true if config was modified and should be persisted.
   */
  migrateLegacyModels() {
    const LEGACY_DEEPSEEK = /* @__PURE__ */ new Set(["deepseek-chat", "deepseek-reasoner"]);
    const DEEPSEEK_TARGET = "deepseek-v4-flash";
    const LEGACY_KIMI = /* @__PURE__ */ new Set(["kimi-k2-0711-preview", "kimi-k2-turbo-preview"]);
    const KIMI_TARGET = "kimi-k2.6";
    let changed = false;
    const dm = this.config.defaultModels;
    if (dm) {
      if (LEGACY_DEEPSEEK.has(dm.deepseek)) {
        dm.deepseek = DEEPSEEK_TARGET;
        changed = true;
      }
      if (LEGACY_KIMI.has(dm.kimi)) {
        dm.kimi = KIMI_TARGET;
        changed = true;
      }
    }
    const mp = this.config.modelParams;
    if (mp) {
      for (const legacy of LEGACY_DEEPSEEK) {
        if (legacy in mp) {
          if (!(DEEPSEEK_TARGET in mp)) {
            mp[DEEPSEEK_TARGET] = mp[legacy];
          }
          delete mp[legacy];
          changed = true;
        }
      }
      for (const legacy of LEGACY_KIMI) {
        if (legacy in mp) {
          if (!(KIMI_TARGET in mp)) {
            mp[KIMI_TARGET] = mp[legacy];
          }
          delete mp[legacy];
          changed = true;
        }
      }
    }
    if (changed) {
      process.stderr.write(
        `[ai-cli] Migrated retired model ids in config.json (deepseek \u2192 ${DEEPSEEK_TARGET}, kimi \u2192 ${KIMI_TARGET})
`
      );
    }
    return changed;
  }
  save() {
    mkdirSync(this.configDir, { recursive: true });
    writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), "utf-8");
  }
  getApiKey(providerId) {
    const envKey = EnvLoader.getApiKey(providerId);
    if (envKey) return envKey;
    return this.config.apiKeys[providerId];
  }
  setApiKey(providerId, key) {
    this.config.apiKeys[providerId] = key;
    this.save();
  }
  get(key) {
    return this.config[key];
  }
  set(key, value) {
    this.config[key] = value;
    this.save();
  }
  /**
   * 仅修改内存中的配置值，不持久化到磁盘。
   * 用于 CLI 命令行参数覆盖（-p, -m, --no-stream），使其只对当前进程生效。
   *
   * M6 fix (v0.4.107)：原版直接赋值不走 Zod，调用点必须自律确保来源可信
   *   （如 CLI flag、global config）。如果未来谁把外部输入接到这里，类型系统
   *   不会阻止恶意值偷渡进 config。现在用 partial Zod parse 校验单字段，
   *   失败时抛 ConfigError。setTransient 永远不接受外部不可信输入——这是
   *   「调用方信任」契约的硬实现。
   */
  setTransient(key, value) {
    const draft = { ...this.config, [key]: value };
    const result = ConfigSchema.safeParse(draft);
    if (!result.success) {
      const firstErr = result.error.errors[0];
      throw new ConfigError(
        `Invalid transient config value for "${String(key)}": ${firstErr?.message ?? "validation failed"}`
      );
    }
    this.config = result.data;
  }
  isFirstRun() {
    return !existsSync(this.configPath);
  }
  getConfigDir() {
    return this.configDir;
  }
  getHistoryDir() {
    return join(this.configDir, HISTORY_DIR_NAME);
  }
  getPluginsDir() {
    return join(this.configDir, PLUGINS_DIR_NAME);
  }
  getDefaultProvider() {
    return EnvLoader.getDefaultProvider() ?? this.config.defaultProvider;
  }
  /** 点分路径读取配置值，如 `ui.theme` → config.ui.theme */
  getByPath(path) {
    const keys = path.split(".");
    let current = this.config;
    for (const key of keys) {
      if (current == null || typeof current !== "object") return void 0;
      current = current[key];
    }
    return current;
  }
  /** 点分路径写入配置值，自动类型转换（boolean/number/string）并持久化 */
  setByPath(path, rawValue) {
    const keys = path.split(".");
    if (keys.length === 0) return;
    let value = rawValue;
    if (rawValue === "true") value = true;
    else if (rawValue === "false") value = false;
    else if (rawValue !== "" && !isNaN(Number(rawValue))) value = Number(rawValue);
    const draft = JSON.parse(JSON.stringify(this.config));
    let current = draft;
    for (let i = 0; i < keys.length - 1; i++) {
      const key = keys[i];
      if (current[key] == null || typeof current[key] !== "object") {
        current[key] = {};
      }
      current = current[key];
    }
    current[keys[keys.length - 1]] = value;
    const result = ConfigSchema.safeParse(draft);
    if (!result.success) {
      const firstErr = result.error.errors[0];
      throw new ConfigError(`Invalid config value for "${path}": ${firstErr?.message ?? "validation failed"}`);
    }
    this.config = result.data;
    this.save();
  }
  /** 获取完整配置对象的格式化 JSON 字符串（用于 /config show 等展示） */
  toFormattedJSON() {
    return JSON.stringify(this.config, null, 2);
  }
  /** 获取完整配置对象（JSON 兼容的原始对象） */
  toJSON() {
    return structuredClone(this.config);
  }
};

export {
  ConfigManager
};
