import os from 'os';
import './load-env';

import {
  AUTH_COOKIE_NAME,
  AUTH_COOKIE_PATH,
  AUTH_REFRESH_COOKIE_NAME,
  AUTH_PROVIDERS,
  BCRYPT_SALT_ROUNDS,
  DEFAULT_AUTH_COOKIE_SAME_SITE,
  JWT_EXPIRES_IN_MS,
  REFRESH_TOKEN_BYTES,
  REFRESH_TOKEN_EXPIRES_IN_MS,
  REFRESH_TOKEN_HASH_ALGORITHM,
} from '@/shared/constants/auth';
import {
  DEFAULT_DEV_API_PORT,
  DEFAULT_DEV_UI_PORT,
  DEFAULT_DEV_HOST,
  DEFAULT_DEV_SECRET_KEY,
  DEFAULT_EMAIL_FROM,
  DEFAULT_EMAIL_HOST,
  DEFAULT_EMAIL_PORT,
} from '@/shared/constants/app';
import { ROLE_NAMES } from '@/shared/constants/roles';

/**
 * Returns env var value, falling back to devDefault only in development.
 * Throws in production/dev_stage if the env var is not set.
 */
function requiredEnvWithDevDefault(name: string, devDefault: string): string {
  const value = process.env[name];

  if (value) {
    return value;
  }

  const env = process.env.NODE_ENV;
  if (env === 'production' || env === 'dev_stage') {
    throw new Error(
      `Missing required environment variable: ${name} (no dev defaults in ${env})`,
    );
  }

  return devDefault;
}

function readBooleanEnv(name: string, defaultValue: boolean): boolean {
  const value = process.env[name];

  if (value === undefined || value === '') {
    return defaultValue;
  }

  if (value === 'true') {
    return true;
  }

  if (value === 'false') {
    return false;
  }

  throw new Error(`Invalid boolean environment variable: ${name}`);
}

function readNumberEnv(name: string, defaultValue: number): number {
  const value = process.env[name];

  if (value === undefined || value === '') {
    return defaultValue;
  }

  const parsedValue = Number(value);

  if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
    throw new Error(`Invalid positive number environment variable: ${name}`);
  }

  return parsedValue;
}

function readListEnv(name: string, defaultValue: string[]): string[] {
  const value = process.env[name];

  if (!value) {
    return defaultValue;
  }

  return value
    .split(',')
    .map((entry) => entry.trim())
    .filter(Boolean);
}

function normalizeSameSite(value: string): 'strict' | 'lax' | 'none' {
  const normalizedValue = value.toLowerCase();

  if (
    normalizedValue !== 'strict' &&
    normalizedValue !== 'lax' &&
    normalizedValue !== 'none'
  ) {
    throw new Error(`Invalid AUTH_COOKIE_SAME_SITE value: ${value}`);
  }

  return normalizedValue;
}

const isProductionLike =
  process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'dev_stage';
const isProduction = process.env.NODE_ENV === 'production';
const authCookieSameSite = normalizeSameSite(
  process.env.AUTH_COOKIE_SAME_SITE || DEFAULT_AUTH_COOKIE_SAME_SITE,
);
const authCookieSecure = readBooleanEnv('AUTH_COOKIE_SECURE', isProductionLike);
const authCookieMaxAgeMs = readNumberEnv('AUTH_COOKIE_MAX_AGE_MS', JWT_EXPIRES_IN_MS);
const defaultApiOrigin = `${DEFAULT_DEV_HOST}:${process.env.PORT || DEFAULT_DEV_API_PORT}`;
const defaultUiOrigin = `${process.env.UI_HOST || DEFAULT_DEV_HOST}${
  process.env.UI_PORT || DEFAULT_DEV_UI_PORT
    ? `:${process.env.UI_PORT || DEFAULT_DEV_UI_PORT}`
    : ''
}`;
const defaultAllowedOrigins = [defaultUiOrigin, defaultApiOrigin];
const allowedOrigins = [
  ...new Set(readListEnv('ALLOWED_ORIGINS', defaultAllowedOrigins)),
];
// The staging preview (dev_stage) runs behind a tunnel on a dynamic domain that
// the platform does not inject as ALLOWED_ORIGINS. When the list is not set
// explicitly there, reflect the request origin instead of rejecting it. Strict
// production still requires an explicit allow-list (enforced below).
const allowAllOrigins =
  isProductionLike && !isProduction && !process.env.ALLOWED_ORIGINS;

if (authCookieSameSite === 'none' && !authCookieSecure) {
  throw new Error(
    'AUTH_COOKIE_SECURE must be true when AUTH_COOKIE_SAME_SITE is none',
  );
}

if (isProductionLike && !authCookieSecure) {
  throw new Error(
    'AUTH_COOKIE_SECURE must be true in production-like environments',
  );
}

if (isProduction && !process.env.ALLOWED_ORIGINS) {
  throw new Error('ALLOWED_ORIGINS must be configured in production');
}

const remote = '';
const port = isProduction ? '' : process.env.PORT || DEFAULT_DEV_API_PORT;
const serverPort =
  process.env.NODE_ENV === 'dev_stage'
    ? DEFAULT_DEV_UI_PORT
    : process.env.PORT || DEFAULT_DEV_API_PORT;
const hostUI = isProduction ? '' : process.env.UI_HOST || DEFAULT_DEV_HOST;
const portUI = isProduction ? '' : process.env.UI_PORT || DEFAULT_DEV_UI_PORT;
const portUIProd = isProduction
  ? ''
  : `:${process.env.UI_PORT || DEFAULT_DEV_UI_PORT}`;
const swaggerUI = isProduction
  ? ''
  : process.env.SWAGGER_HOST || DEFAULT_DEV_HOST;
const swaggerPort = isProduction
  ? ''
  : `:${process.env.SWAGGER_PORT || DEFAULT_DEV_API_PORT}`;
const host = isProduction ? remote : DEFAULT_DEV_HOST;
const apiUrl = `${host}${port ? `:${port}` : ''}/api`;
const swaggerUrl = `${swaggerUI}${swaggerPort}`;
const uiUrl = `${hostUI}${portUI ? `:${portUI}` : ''}/#`;
const backUrl = `${hostUI}${portUI ? `:${portUI}` : ''}`;

const config = {
  gcloud: {
    bucket: process.env.GCLOUD_BUCKET || '',
    hash: process.env.GCLOUD_HASH || '',
  },
  bcrypt: {
    saltRounds: BCRYPT_SALT_ROUNDS,
  },
  providers: AUTH_PROVIDERS,
  auth: {
    allowedOrigins,
    allowAllOrigins,
    accessCookieName: process.env.AUTH_ACCESS_COOKIE_NAME || AUTH_COOKIE_NAME,
    accessTokenMaxAgeMs: authCookieMaxAgeMs,
    cookieDomain: process.env.AUTH_COOKIE_DOMAIN || undefined,
    cookiePath: AUTH_COOKIE_PATH,
    cookieSameSite: authCookieSameSite,
    cookieSecure: authCookieSecure,
    refreshCookieName:
      process.env.AUTH_REFRESH_COOKIE_NAME || AUTH_REFRESH_COOKIE_NAME,
    refreshTokenBytes: REFRESH_TOKEN_BYTES,
    refreshTokenHashAlgorithm: REFRESH_TOKEN_HASH_ALGORITHM,
    refreshTokenMaxAgeMs: readNumberEnv(
      'AUTH_REFRESH_TOKEN_MAX_AGE_MS',
      REFRESH_TOKEN_EXPIRES_IN_MS,
    ),
    // How long an expired/rotated refresh-token row is physically retained past
    // its `expiresAt` before the maintenance job deletes it. The grace window
    // keeps recently-expired rows around for reuse-detection forensics; once a
    // token is past `expiresAt` it can no longer be presented (the cookie is
    // expired), so deletion after the window is safe. Default: 7 days.
    refreshTokenRetentionMs: readNumberEnv(
      'AUTH_REFRESH_TOKEN_RETENTION_MS',
      7 * 24 * 60 * 60 * 1000,
    ),
  },
  secret_key: requiredEnvWithDevDefault('SECRET_KEY', DEFAULT_DEV_SECRET_KEY),
  remote,
  port,
  serverPort,
  hostUI,
  portUI,
  portUIProd,
  swaggerUI,
  swaggerPort,
  google: {
    clientId: process.env.GOOGLE_CLIENT_ID || '',
    clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
  },
  uploadDir: os.tmpdir(),
  email: {
    from: process.env.EMAIL_FROM || DEFAULT_EMAIL_FROM,
    host: process.env.EMAIL_HOST || DEFAULT_EMAIL_HOST,
    port: Number(process.env.EMAIL_PORT || DEFAULT_EMAIL_PORT),
    auth: {
      user: process.env.EMAIL_USER || '',
      pass: process.env.EMAIL_PASS,
    },
    tls: {
      rejectUnauthorized: false,
    },
  },
  roles: {
    super_admin: ROLE_NAMES.SUPER_ADMIN,
    admin: ROLE_NAMES.SYSTEM_ADMIN,
    user: ROLE_NAMES.OFFICE_MANAGER,
  },
  host,
  apiUrl,
  swaggerUrl,
  uiUrl,
  backUrl,
};

export default config;
