/**
 * automation/core/env.ts
 *
 * 自动化服务的环境变量 schema 与启动期校验。
 *
 * 用法：
 *   1. 在服务器入口最早处 import 本文件，让模块求值即触发校验：
 *        import "dotenv/config";
 *        import "./core/env.ts";   // ← 缺/坏的 env 在这里 fail-fast
 *   2. 运行时需要读 env 的代码可以走 `getEnv()`，拿到强类型对象：
 *        import { getEnv } from "../core/env.ts";
 *        const port = getEnv().automationPort;
 *
 * 注意：本 schema 只覆盖**核心**变量（Supabase / Prompt store / LLM /
 * 自动化运行参数）。Playwright 子进程使用的 SEARCH_* 等变量可继续
 * 通过 process.env 直接读，待后续 PR 收敛。
 *
 * Supabase URL/Key 在多个旧字段间回退（`resolveSupabaseUrl/Key`），
 * 因此本模块委托给 supabase-node-client.js 做解析，再断言非空。
 */

import process from "node:process";
import { z } from "zod";

import {
  resolveSupabaseKey,
  resolveSupabaseUrl,
} from "../internal/supabase-node-client.js";

// ── 输入预处理 ──────────────────────────────────────────────────
//
// 数字与字符串 env 都可能以空字符串到达（来自 .env 占位符）。
// 我们在 schema 之前把它们规范化：空串/缺失统一替换为 fallback；
// 字符串 trim 后空串视为 undefined。

function parseStringEnv(value: string | undefined): string | undefined {
  if (typeof value !== "string") return undefined;
  const trimmed = value.trim();
  return trimmed.length === 0 ? undefined : trimmed;
}

function parseNumericEnv(
  value: string | undefined,
  fallback: number,
): number {
  if (value === undefined || value === null) return fallback;
  const trimmed = String(value).trim();
  if (trimmed.length === 0) return fallback;
  const parsed = Number(trimmed);
  // NaN 会在 schema 阶段被 z.number().refine() 拒绝并带上字段路径。
  return Number.isFinite(parsed) ? parsed : Number.NaN;
}

// ── Schema ────────────────────────────────────────────────────

const positiveInt = z
  .number()
  .refine((n) => Number.isInteger(n) && n > 0, { message: "必须是正整数" });

const nonNegativeInt = z
  .number()
  .refine((n) => Number.isInteger(n) && n >= 0, { message: "必须是非负整数" });

export const LLM_STAGES = [
  "document_ocr",
  "document_extraction",
  "standardization",
  "keyword_semantic",
  "search_vision",
  "fixed_search_batch",
  "chatbot_intent",
] as const;

export type LlmStage = (typeof LLM_STAGES)[number];

export const LLM_PROVIDER_KEYS = [
  "deepseek",
  "glm",
  "gemini",
  "openai",
] as const;

export type LlmProviderKey = (typeof LLM_PROVIDER_KEYS)[number] | string;

const llmProviderCredentialsSchema = z.record(z.object({
  baseURL: z.string(),
  apiKey: z.string(),
}).optional());

const llmStageProvidersSchema = z.object({
  document_ocr: z.string().optional(),
  document_extraction: z.string().optional(),
  standardization: z.string().optional(),
  keyword_semantic: z.string().optional(),
  search_vision: z.string().optional(),
  fixed_search_batch: z.string().optional(),
  chatbot_intent: z.string().optional(),
});

const llmStageModelsSchema = z.object({
  document_ocr: z.string().optional(),
  document_extraction: z.string().optional(),
  standardization: z.string().optional(),
  keyword_semantic: z.string().optional(),
  search_vision: z.string().optional(),
  fixed_search_batch: z.string().optional(),
  chatbot_intent: z.string().optional(),
});

const envSchema = z.object({
  // — HTTP server —
  automationPort: positiveInt,

  // — Prompt store（独立 Supabase，可选；为空时由 prompt-runtime 回退到主 Supabase） —
  promptSupabaseUrl: z.string().optional(),
  promptSupabaseServiceRoleKey: z.string().optional(),
  promptEnv: z.string(),
  promptCacheTtlMs: nonNegativeInt,

  // — LLM（可选；缺失时 createLlmClient 会抛错，createVisionClientFromEnv 返回 null） —
  llmBaseUrl: z.string().optional(),
  llmApiKey: z.string().optional(),
  llmModel: z.string().optional(),
  llmProvider: z.string().optional(),
  llmProviderCredentials: llmProviderCredentialsSchema,
  llmStageProviders: llmStageProvidersSchema,
  llmStageModels: llmStageModelsSchema,
  llmQueueMinIntervalMs: nonNegativeInt,
  llmRetryMaxAttempts: positiveInt,
  llmRetryBaseDelayMs: nonNegativeInt,

  // — 自动化调度 —
  standardizeConcurrency: positiveInt,

  // — Supabase（必填；通过 resolveSupabaseUrl/Key 在多个旧字段间回退） —
  supabaseUrl: z
    .string()
    .min(1, "缺少 SUPABASE_URL（或回退别名 VITE_SUPABASE_URL）"),
  supabaseKey: z
    .string()
    .min(
      1,
      "缺少 SUPABASE_SERVICE_ROLE_KEY（或别名 SUPABASE_service_key / VITE_SUPABASE_SERVICE_ROLE_KEY / SUPABASE_ANON_KEY）",
    ),
});

export type AutomationEnv = z.infer<typeof envSchema>;

// ── 解析与缓存 ────────────────────────────────────────────────

let cachedEnv: AutomationEnv | null = null;

function parseProviderCredential(
  source: NodeJS.ProcessEnv,
  prefix: string,
) {
  const baseURL = parseStringEnv(source[`${prefix}_LLM_BASE_URL`]);
  const apiKey = parseStringEnv(source[`${prefix}_LLM_API_KEY`]);
  if (!baseURL || !apiKey) {
    return undefined;
  }
  return { baseURL, apiKey };
}

function buildEnvInput(source: NodeJS.ProcessEnv) {
  const llmProviderCredentials = Object.fromEntries(
    LLM_PROVIDER_KEYS
      .map((provider) => [provider, parseProviderCredential(source, provider.toUpperCase())] as const)
      .filter((entry) => Boolean(entry[1])),
  );

  return {
    automationPort: parseNumericEnv(source.AUTOMATION_PORT, 3030),
    promptSupabaseUrl: parseStringEnv(source.PROMPT_SUPABASE_URL),
    promptSupabaseServiceRoleKey: parseStringEnv(
      source.PROMPT_SUPABASE_SERVICE_ROLE_KEY,
    ),
    promptEnv: parseStringEnv(source.PROMPT_ENV) ?? "prod",
    promptCacheTtlMs: parseNumericEnv(source.PROMPT_CACHE_TTL_MS, 300_000),
    llmBaseUrl: parseStringEnv(source.LLM_BASE_URL),
    llmApiKey: parseStringEnv(source.LLM_API_KEY),
    llmModel: parseStringEnv(source.LLM_MODEL),
    llmProvider: parseStringEnv(source.LLM_PROVIDER),
    llmProviderCredentials,
    llmStageProviders: {
      document_ocr: parseStringEnv(source.DOCUMENT_OCR_LLM_PROVIDER),
      document_extraction: parseStringEnv(source.DOCUMENT_EXTRACTION_LLM_PROVIDER),
      standardization: parseStringEnv(source.STANDARDIZATION_LLM_PROVIDER),
      keyword_semantic: parseStringEnv(source.KEYWORD_SEMANTIC_LLM_PROVIDER),
      search_vision: parseStringEnv(source.SEARCH_VISION_LLM_PROVIDER),
      fixed_search_batch: parseStringEnv(source.FIXED_SEARCH_LLM_PROVIDER),
      chatbot_intent: parseStringEnv(source.CHATBOT_INTENT_LLM_PROVIDER),
    },
    llmStageModels: {
      document_ocr: parseStringEnv(source.DOCUMENT_OCR_LLM_MODEL),
      document_extraction: parseStringEnv(source.DOCUMENT_EXTRACTION_LLM_MODEL),
      standardization: parseStringEnv(source.STANDARDIZATION_LLM_MODEL),
      keyword_semantic: parseStringEnv(source.KEYWORD_SEMANTIC_LLM_MODEL),
      search_vision:
        parseStringEnv(source.SEARCH_VISION_LLM_MODEL)
        ?? parseStringEnv(source.SEARCH_VISION_MODEL),
      fixed_search_batch: parseStringEnv(source.FIXED_SEARCH_LLM_MODEL),
      chatbot_intent: parseStringEnv(source.CHATBOT_INTENT_LLM_MODEL),
    },
    llmQueueMinIntervalMs: parseNumericEnv(source.LLM_QUEUE_MIN_INTERVAL_MS, 3000),
    llmRetryMaxAttempts: parseNumericEnv(source.LLM_RETRY_MAX_ATTEMPTS, 4),
    llmRetryBaseDelayMs: parseNumericEnv(source.LLM_RETRY_BASE_DELAY_MS, 1500),
    standardizeConcurrency: parseNumericEnv(source.STANDARDIZE_CONCURRENCY, 1),
    supabaseUrl: resolveSupabaseUrl(source),
    supabaseKey: resolveSupabaseKey(source),
  };
}

function formatZodIssues(issues: z.ZodIssue[]): string {
  return issues
    .map((issue) => {
      const path = issue.path.join(".") || "(root)";
      return `  • ${path}: ${issue.message}`;
    })
    .join("\n");
}

/**
 * 解析并校验当前 process.env，返回类型化对象。
 * 同一进程内可重复调用：第二次起返回缓存。
 * 校验失败时抛出带详细路径的 Error，由调用方决定退出策略。
 */
export function loadEnv(source: NodeJS.ProcessEnv = process.env): AutomationEnv {
  if (cachedEnv) {
    return cachedEnv;
  }

  const result = envSchema.safeParse(buildEnvInput(source));
  if (!result.success) {
    throw new Error(
      "环境变量校验失败，请检查 .env：\n" +
        formatZodIssues(result.error.issues) +
        "\n参考模板：.env.example",
    );
  }

  cachedEnv = result.data;
  return cachedEnv;
}

/** 强制刷新缓存（仅供测试使用）。 */
export function resetEnvCacheForTesting(): void {
  cachedEnv = null;
}

/**
 * 取已校验的 env。首次调用时执行 loadEnv()。
 */
export function getEnv(): AutomationEnv {
  return cachedEnv ?? loadEnv();
}

// ── 模块求值期立即校验，让缺/坏的 env 在 import 阶段暴露 ───────
// 测试环境（vitest / NODE_ENV=test）跳过自动校验，由测试自行决定何时调用 loadEnv()。
if (!process.env.VITEST && process.env.NODE_ENV !== "test") {
  loadEnv();
}
