// backend/src/config/configuration.ts

/** Matches values accepted by jsonwebtoken `expiresIn` (via ms), e.g. 7d, 15m, or plain seconds. */
const JWT_TIMESPAN_PATTERN = /^\d+(\.\d+)?(ms|s|m|h|d|w|y)?$/i;

function assertJwtSecret(value: string, envKey: string): string {
  const trimmed = value.trim();
  if (!trimmed) {
    throw new Error(
      `❌ Environment variable ${envKey} is required but not set`,
    );
  }
  if (trimmed.length < 16) {
    throw new Error(`❌ ${envKey} must be at least 16 characters`);
  }
  if (/CHANGE_ME/i.test(trimmed)) {
    throw new Error(`❌ ${envKey} must be changed from the placeholder value`);
  }
  return trimmed;
}

function assertJwtTimespan(value: string, envKey: string): string {
  const trimmed = value.trim();
  if (!trimmed) {
    throw new Error(
      `❌ Environment variable ${envKey} is required but not set`,
    );
  }
  if (!/^\d+$/.test(trimmed) && !JWT_TIMESPAN_PATTERN.test(trimmed)) {
    throw new Error(
      `❌ ${envKey}="${value}" is invalid. Use a duration like 7d, 15m, 30d, or a number of seconds.`,
    );
  }
  return trimmed;
}

function parseEnvBoolean(
  value: string | undefined,
  defaultValue: boolean,
): boolean {
  if (value === undefined || value.trim() === '') {
    return defaultValue;
  }
  const normalized = value.trim().toLowerCase();
  if (['1', 'true', 'yes', 'on'].includes(normalized)) {
    return true;
  }
  if (['0', 'false', 'no', 'off'].includes(normalized)) {
    return false;
  }
  return defaultValue;
}

export interface Config {
  port: number;
  database: {
    url: string;
  };
  jwt: {
    secret: string;
    expiresIn: string;
    refreshSecret: string;
    refreshExpiresIn: string;
  };
  cookie: {
    secure: boolean;
  };
  throttle: {
    ttl: number;
    limit: number;
  };
  sms: {
    apiKey: string | null;
    templateId: number;
  };
  voice: VoiceConfig;
}

/** Only OpenRouter today. The indirection exists so a locale can diverge without code. */
export type VoiceProviderId = 'openrouter';

export type VoiceProfile = {
  asr: { provider: VoiceProviderId; model: string };
  llm: { provider: VoiceProviderId; model: string };
};

export interface VoiceConfig {
  openRouter: {
    apiKey: string | null;
    baseUrl: string;
  };
  /**
   * Locale → provider profile. A locale absent here has no microphone button at all —
   * clean absence rather than a dead control.
   */
  profiles: Record<string, VoiceProfile>;
  /** Recording cap in ms; null means uncapped. */
  maxRecordingMs: number | null;
  throttle: { ttl: number; limit: number };
}

export const VOICE_LOCALES = ['fa', 'en', 'nl'] as const;

const DEFAULT_ASR_MODEL = 'openai/whisper-1';
const DEFAULT_LLM_MODEL = 'google/gemini-3.7-flash';

export default (): Config => {
  // Helper function to get required env var with type safety
  const getEnvVar = (key: string): string => {
    const value = process.env[key];
    if (!value) {
      throw new Error(`❌ Environment variable ${key} is required but not set`);
    }
    return value;
  };

  // Helper for optional env vars with defaults (whitespace-only counts as unset)
  const getEnvVarWithDefault = (key: string, defaultValue: string): string => {
    const value = process.env[key]?.trim();
    return value ? value : defaultValue;
  };

  const getEnvVarAsNumber = (key: string, defaultValue: number): number => {
    const value = process.env[key];
    if (!value) return defaultValue;
    const parsed = parseInt(value, 10);
    return isNaN(parsed) ? defaultValue : parsed;
  };

  const jwtSecret = assertJwtSecret(getEnvVar('JWT_SECRET'), 'JWT_SECRET');
  const jwtExpiresIn = assertJwtTimespan(
    getEnvVarWithDefault('JWT_EXPIRES_IN', '7d'),
    'JWT_EXPIRES_IN',
  );
  const jwtRefreshSecret = assertJwtSecret(
    getEnvVarWithDefault('JWT_REFRESH_SECRET', jwtSecret),
    'JWT_REFRESH_SECRET',
  );
  const jwtRefreshExpiresIn = assertJwtTimespan(
    getEnvVarWithDefault('JWT_REFRESH_EXPIRES_IN', '30d'),
    'JWT_REFRESH_EXPIRES_IN',
  );
  const cookieSecure = parseEnvBoolean(process.env.COOKIE_SECURE, false);

  return {
    port: getEnvVarAsNumber('PORT', 3000),
    database: {
      url: getEnvVar('DATABASE_URL'),
    },
    jwt: {
      secret: jwtSecret,
      expiresIn: jwtExpiresIn,
      refreshSecret: jwtRefreshSecret,
      refreshExpiresIn: jwtRefreshExpiresIn,
    },
    cookie: {
      secure: cookieSecure,
    },
    throttle: {
      ttl: getEnvVarAsNumber('THROTTLE_TTL', 60),
      limit: getEnvVarAsNumber('THROTTLE_LIMIT', 100),
    },
    sms: {
      apiKey: process.env.SMS_IR_API_KEY?.trim() || null,
      templateId: getEnvVarAsNumber('SMS_IR_TEMPLATE_ID', 123456),
    },
    voice: buildVoiceConfig(getEnvVarWithDefault, getEnvVarAsNumber),
  };
};

const VOICE_PROVIDER_IDS: readonly VoiceProviderId[] = ['openrouter'];

/** Unknown provider ids fail at boot; silently coercing a typo would ship the wrong vendor. */
function parseProviderId(
  value: string | undefined,
  envKey: string,
): VoiceProviderId {
  const trimmed = value?.trim();
  if (!trimmed) return 'openrouter';
  if ((VOICE_PROVIDER_IDS as readonly string[]).includes(trimmed)) {
    return trimmed as VoiceProviderId;
  }
  throw new Error(
    `❌ ${envKey}="${trimmed}" is not a known voice provider (${VOICE_PROVIDER_IDS.join(', ')})`,
  );
}

/**
 * Every enabled locale gets its own ASR and LLM provider+model, each independently
 * overridable. They all point at the same OpenRouter models today; the per-locale
 * indirection stays so a locale can diverge by configuration rather than by code.
 */
function buildVoiceConfig(
  getEnvVarWithDefault: (key: string, defaultValue: string) => string,
  getEnvVarAsNumber: (key: string, defaultValue: number) => number,
): VoiceConfig {
  const enabled = getEnvVarWithDefault(
    'VOICE_ENABLED_LOCALES',
    VOICE_LOCALES.join(','),
  )
    .split(',')
    .map((locale) => locale.trim().toLowerCase())
    .filter(Boolean);
  for (const locale of enabled) {
    // Fail loudly, like parseProviderId. Silently filtering a typo would disable the
    // microphone everywhere with nothing to explain why.
    if (!(VOICE_LOCALES as readonly string[]).includes(locale)) {
      throw new Error(
        `❌ VOICE_ENABLED_LOCALES contains unknown locale "${locale}" (known: ${VOICE_LOCALES.join(', ')})`,
      );
    }
  }

  const profiles: Record<string, VoiceProfile> = {};
  for (const locale of enabled) {
    const suffix = locale.toUpperCase();
    profiles[locale] = {
      asr: {
        provider: parseProviderId(
          process.env[`VOICE_ASR_PROVIDER_${suffix}`],
          `VOICE_ASR_PROVIDER_${suffix}`,
        ),
        model: getEnvVarWithDefault(
          `VOICE_ASR_MODEL_${suffix}`,
          getEnvVarWithDefault('VOICE_ASR_MODEL', DEFAULT_ASR_MODEL),
        ),
      },
      llm: {
        provider: parseProviderId(
          process.env[`VOICE_LLM_PROVIDER_${suffix}`],
          `VOICE_LLM_PROVIDER_${suffix}`,
        ),
        model: getEnvVarWithDefault(
          `VOICE_LLM_MODEL_${suffix}`,
          getEnvVarWithDefault('VOICE_LLM_MODEL', DEFAULT_LLM_MODEL),
        ),
      },
    };
  }

  const maxRecordingMs = getEnvVarAsNumber('VOICE_MAX_RECORDING_MS', 120_000);

  return {
    openRouter: {
      apiKey: process.env.OPENROUTER_API_KEY?.trim() || null,
      baseUrl: getEnvVarWithDefault(
        'OPENROUTER_BASE_URL',
        'https://openrouter.ai/api/v1',
      ),
    },
    profiles,
    maxRecordingMs: maxRecordingMs > 0 ? maxRecordingMs : null,
    throttle: {
      ttl: getEnvVarAsNumber('VOICE_THROTTLE_TTL', 60),
      limit: getEnvVarAsNumber('VOICE_THROTTLE_LIMIT', 6),
    },
  };
}
