import { z } from 'zod';

const envSchema = z
  .object({
    // Database
    DATABASE_URL: z.string().url().startsWith('postgresql://'),

    // Redis
    REDIS_URL: z.string().url().startsWith('redis://'),

    // Auth
    BETTER_AUTH_SECRET: z.string().min(32),
    BETTER_AUTH_URL: z.string().url(),
    CSRF_SECRET: z.string().min(32),

    // ─── Tenant-configurable (admin UI is canonical; env is fallback) ─────
    // The settings registry at src/lib/settings/registry.ts wires each of
    // these into the per-port admin UI with port → global → env → default
    // precedence. They're optional here so a fresh deploy without an env
    // file can still boot - the operator configures everything via
    // /admin/<integration> after first super-admin login. See
    // docs/superpowers/specs/2026-05-15-env-to-admin-migration-design.md.

    // MinIO / S3 (storage backend) - admin: /admin/storage
    MINIO_ENDPOINT: z.string().min(1).optional(),
    MINIO_PORT: z.coerce.number().int().positive().optional(),
    MINIO_ACCESS_KEY: z.string().min(1).optional(),
    MINIO_SECRET_KEY: z.string().min(1).optional(),
    MINIO_BUCKET: z.string().min(1).optional(),
    MINIO_USE_SSL: z
      .enum(['true', 'false'])
      .optional()
      .transform((v) => (v == null ? undefined : v === 'true')),
    // Server-side-encryption algorithm for S3 PutObject (e.g. 'AES256' or
    // 'aws:kms'). UNSET = no SSE header — required for MinIO/S3 backends
    // without KMS/KES, which reject the header with NotImplemented. The
    // per-port `storage_s3_sse` setting overrides this fallback.
    STORAGE_S3_SSE: z.string().min(1).optional(),

    // Documenso - admin: /admin/documenso
    DOCUMENSO_API_URL: z.string().url().optional(),
    DOCUMENSO_API_KEY: z.string().min(1).optional(),
    DOCUMENSO_API_VERSION: z.enum(['v1', 'v2']).default('v1'),
    DOCUMENSO_WEBHOOK_SECRET: z.string().min(16).optional(),
    DOCUMENSO_TEMPLATE_ID_EOI: z.coerce.number().int().positive().optional(),
    DOCUMENSO_CLIENT_RECIPIENT_ID: z.coerce.number().int().positive().optional(),
    DOCUMENSO_DEVELOPER_RECIPIENT_ID: z.coerce.number().int().positive().optional(),
    DOCUMENSO_APPROVAL_RECIPIENT_ID: z.coerce.number().int().positive().optional(),

    // Email / SMTP - admin: /admin/email
    SMTP_HOST: z.string().min(1).optional(),
    SMTP_PORT: z.coerce.number().int().positive().optional(),
    SMTP_USER: z.string().optional(),
    SMTP_PASS: z.string().optional(),
    SMTP_FROM: z.string().optional(),
    // Dev/test safety net: when set, sendEmail redirects every outbound message
    // to this address regardless of the requested recipient. Leave empty in prod
    // UNLESS deliberately enabling the beta redirect (see ALLOW_PROD_EMAIL_REDIRECT).
    EMAIL_REDIRECT_TO: z.string().email().optional(),
    // Explicit opt-in to allow EMAIL_REDIRECT_TO in production. The prod guard
    // below normally refuses to boot with a redirect set (footgun: silently
    // funnels all customer mail to one inbox). During beta we WANT that — every
    // outbound message goes to the operator inbox so nothing reaches real
    // clients by accident. Must be set together with EMAIL_REDIRECT_TO.
    ALLOW_PROD_EMAIL_REDIRECT: z
      .enum(['true', 'false'])
      .default('false')
      .transform((v) => v === 'true'),

    // Encryption
    EMAIL_CREDENTIAL_KEY: z
      .string()
      .length(64)
      .regex(/^[0-9a-f]+$/i, 'Must be a 64-character hex string'),

    // Google OAuth (optional)
    GOOGLE_CLIENT_ID: z.string().optional(),
    GOOGLE_CLIENT_SECRET: z.string().optional(),

    // Shared secret used by the marketing website's server-side dual-write
    // helper (POST to /api/public/website-inquiries). Set the SAME value on
    // the website's CRM_INTAKE_SECRET env. Leave unset in dev/staging until
    // the website's CRM_INTAKE_URL is also set - without this, the public
    // intake endpoint refuses every request.
    WEBSITE_INTAKE_SECRET: z.string().min(16).optional(),

    // OpenAI (optional)
    OPENAI_API_KEY: z.string().optional(),

    // Sentry (optional - when unset the SDK is a no-op)
    NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
    SENTRY_ENVIRONMENT: z.string().optional(),
    SENTRY_TRACES_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(0.1),

    // App URLs - admin: /admin/general (TODO once general admin page exists;
    // for now write via the API: PUT /api/v1/admin/settings/app_url)
    APP_URL: z.string().url(),
    PUBLIC_SITE_URL: z.string().url().optional(),
    /**
     * Client-side bundle baseline URL. Inlined at build time by Next, so
     * a missing value at build leaks into the browser as the empty
     * string and forces fallbacks (`window.location.origin`) which
     * silently work in dev and break on multi-origin deploys.
     * build-auditor H2: validate at runtime so the bundle never ships
     * with a blank baseline. The validation runs against
     * `process.env.NEXT_PUBLIC_APP_URL` at build time; missing-at-build
     * produces a clear validation error rather than a confusing
     * runtime fallback.
     */
    NEXT_PUBLIC_APP_URL: z.string().url(),
    NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
    LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
    /**
     * HTTP listener port. zod-coerced from PORT so a typo (`PORT=foo`) hard-
     * fails at boot rather than silently listening on an ephemeral port.
     */
    PORT: z.coerce.number().int().positive().default(3000),
    /**
     * When true, the filesystem storage backend refuses to start (per
     * src/lib/storage/filesystem.ts:192). Reading via the zod schema means
     * a typo on the env var hard-fails at boot rather than silently
     * disabling the multi-node guard. Per CLAUDE.md, multi-node deploys
     * MUST use the s3-compatible backend.
     */
    MULTI_NODE_DEPLOYMENT: z
      .enum(['true', 'false'])
      .default('false')
      .transform((v) => v === 'true'),
  })
  .superRefine((env, ctx) => {
    // CRITICAL safety net: EMAIL_REDIRECT_TO is a dev/test feature that
    // silently rewrites every outbound recipient. Leaving it set in prod
    // funnels every customer email (invites, EOIs, portal magic links,
    // contracts) to a single inbox. The audit caught this had only a
    // `logger.debug` line as forensic trail. Refuse boot when both are
    // simultaneously set in production - UNLESS ALLOW_PROD_EMAIL_REDIRECT=true
    // is set as a deliberate opt-in (beta: route all mail to the operator).
    if (env.NODE_ENV === 'production' && env.EMAIL_REDIRECT_TO && !env.ALLOW_PROD_EMAIL_REDIRECT) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        path: ['EMAIL_REDIRECT_TO'],
        message:
          'EMAIL_REDIRECT_TO must NOT be set in production - it silently rewrites every outbound email recipient. Unset it, or set ALLOW_PROD_EMAIL_REDIRECT=true to deliberately enable it (e.g. beta).',
      });
    }
  });

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

function validateEnv(): Env {
  if (process.env.SKIP_ENV_VALIDATION === '1') {
    return process.env as unknown as Env;
  }

  const result = envSchema.safeParse(process.env);
  if (!result.success) {
    console.error('Invalid environment variables:');
    for (const issue of result.error.issues) {
      console.error(`  ${issue.path.join('.')}: ${issue.message}`);
    }
    process.exit(1);
  }
  return result.data;
}

export const env = validateEnv();
