// services/analytics/src/env.ts
// Strict env validation (Zod) for the analytics ETL service — fail fast at boot,
// no silent crash. Self-contained: this service can't import the Next app's
// `src/env.ts` (separate process / module graph), so it mirrors that style here.
//
// Validation is LAZY (parsed on first `env()` call), not at import time, so that
// importing modules (db.ts, storage.ts) in the test suite — which only sets
// DATABASE_URL and STORAGE_DRIVER=local — never throws on missing R2 vars.
import { z } from "zod";

const schema = z
  .object({
    DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),
    // `local` (tests/dev) reads files from disk; anything else = R2 object store.
    STORAGE_DRIVER: z.string().optional(),
    // Cloudflare R2 (S3-compatible) — required only when not using the local driver.
    R2_ACCOUNT_ID: z.string().optional(),
    R2_BUCKET: z.string().optional(),
    R2_ACCESS_KEY_ID: z.string().optional(),
    R2_SECRET_ACCESS_KEY: z.string().optional(),
    // Shared Bearer secret for POST /jobs. Optional (a boot warning is logged
    // when unset in non-prod; index.ts fails closed in production).
    ANALYTICS_API_KEY: z.string().optional(),
    // Racine du secret de chiffrement des tokens de connecteurs BYOK
    // (src/lib/connectors/crypto.ts dérive la clé AES-256-GCM par HKDF). Requis
    // par le job keyword-enrich pour DÉCHIFFRER le token Mangools via le store
    // (getConnectorToken). Déclaré optionnel ici — crypto.ts lit
    // `process.env.BETTER_AUTH_SECRET` en direct, ce champ ne sert qu'à valider
    // sa présence — mais fourni en prod par l'EnvironmentFile systemd
    // (.env.production, cf. deploy/systemd/seo-insight-analytics.service) et en
    // test par `--env-file=.env.local`. Absent → le déchiffrement échoue
    // (connector_secret_missing) et le job bascule le connecteur en erreur.
    BETTER_AUTH_SECRET: z.string().optional(),
    // Headless render (S7, serp-semantics) — both optional. CHROME_PATH pins
    // an explicit Chrome/Chromium binary (resolveRenderProvider auto-detects
    // otherwise, and degrades to no render if neither resolves).
    // RENDER_MAX_POSITION caps the SERP position eligible for a render
    // fallback (parsed + clamped to [1, 10] by serp-semantics.ts; default 5).
    CHROME_PATH: z.string().optional(),
    RENDER_MAX_POSITION: z.string().optional(),
    // Firecrawl v2 client (crawl on-page, S8) — both optional. Absent →
    // `firecrawlConfigured()` returns false and callers degrade accordingly.
    FIRECRAWL_API_URL: z.string().optional(),
    FIRECRAWL_API_KEY: z.string().optional(),
    // Pool de claim du worker (finding revue firecrawl-onpage) — jobs en vol
    // simultanés. Optionnelle, parsée + clampée à [1, 16] par index.ts (défaut
    // 4) ; toute valeur absente/aberrante retombe sur le défaut, même pattern
    // que RENDER_MAX_POSITION (serp-semantics.ts).
    WORKER_CONCURRENCY: z.string().optional(),
  })
  .superRefine((v, ctx) => {
    if (v.STORAGE_DRIVER !== "local") {
      for (const key of [
        "R2_ACCOUNT_ID",
        "R2_BUCKET",
        "R2_ACCESS_KEY_ID",
        "R2_SECRET_ACCESS_KEY",
      ] as const) {
        if (!v[key]) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            path: [key],
            message: `${key} is required when STORAGE_DRIVER !== "local"`,
          });
        }
      }
    }
  });

export type Env = z.infer<typeof schema>;

let cached: Env | null = null;

function fail(issues: z.ZodIssue[]): never {
  const lines = issues.map(
    (i) => `  - ${i.path.join(".") || "(root)"}: ${i.message}`,
  );
  throw new Error(`[analytics] invalid environment:\n${lines.join("\n")}`);
}

/** Full validation (DATABASE_URL + R2_* unless local). Call at BOOT and from the
 *  R2 storage path — NOT at module-import time, so test files that only set
 *  DATABASE_URL (no STORAGE_DRIVER) don't trip the R2 checks just by importing db.ts. */
export function env(): Env {
  if (cached) return cached;
  const parsed = schema.safeParse(process.env);
  if (!parsed.success) fail(parsed.error.issues);
  cached = parsed.data;
  return cached;
}

/** Narrow accessor for modules that only need the DB connection string (db.ts).
 *  Validates DATABASE_URL alone — never forces R2 vars — so importing db.ts in
 *  tests with a local/unset driver stays cheap and side-effect free. */
export function databaseUrl(): string {
  const parsed = z
    .object({ DATABASE_URL: z.string().min(1, "DATABASE_URL is required") })
    .safeParse(process.env);
  if (!parsed.success) fail(parsed.error.issues);
  return parsed.data.DATABASE_URL;
}

/** Narrow accessor for the headless-render config (CHROME_PATH,
 *  RENDER_MAX_POSITION) — same rationale as `databaseUrl()`: both fields are
 *  optional and unrelated to R2, so serp-semantics.ts must be able to read
 *  them (at module scope, once) without tripping the full `env()` R2
 *  superRefine in tests that never set STORAGE_DRIVER. */
export function renderEnv(): {
  CHROME_PATH?: string;
  RENDER_MAX_POSITION?: string;
} {
  const parsed = z
    .object({
      CHROME_PATH: z.string().optional(),
      RENDER_MAX_POSITION: z.string().optional(),
    })
    .safeParse(process.env);
  if (!parsed.success) fail(parsed.error.issues);
  return parsed.data;
}

/** Narrow accessor for the Firecrawl client config (FIRECRAWL_API_URL,
 *  FIRECRAWL_API_KEY) — same rationale as `renderEnv()`: both fields are
 *  optional and unrelated to R2, so firecrawl.ts must be able to read them
 *  without tripping the full `env()` R2 superRefine in tests that never set
 *  STORAGE_DRIVER. */
export function firecrawlEnv(): {
  FIRECRAWL_API_URL?: string;
  FIRECRAWL_API_KEY?: string;
} {
  const parsed = z
    .object({
      FIRECRAWL_API_URL: z.string().optional(),
      FIRECRAWL_API_KEY: z.string().optional(),
    })
    .safeParse(process.env);
  if (!parsed.success) fail(parsed.error.issues);
  return parsed.data;
}
