import { readFileSync } from 'fs';
import { ConfigLoaderError } from '../errors';
import type { ConfigLoader } from '../types';

interface EnvLoaderOptions {
  convertCase?: boolean;
  parseJson?: boolean;
  parseValues?: boolean;
  nestedDelimiter?: string;
}

export class EnvLoader implements ConfigLoader {
  readonly priority = 100; // Highest priority

  constructor(
    private prefix = '',
    private options: EnvLoaderOptions = {}
  ) {
    this.options = {
      convertCase: false,
      parseJson: true,
      parseValues: true,
      nestedDelimiter: '_',
      ...options,
    };
  }

  load(): Record<string, unknown> {
    try {
      // Load root .env file - try multiple possible locations
      const possiblePaths = ['./.env', '../.env', '../../.env', '../../../.env'];
      for (const path of possiblePaths) {
        this.loadEnvFile(path);
      }

      const config: Record<string, unknown> = {};
      const envVars = process.env;

      for (const [key, value] of Object.entries(envVars)) {
        if (this.prefix && !key.startsWith(this.prefix)) {
          continue;
        }

        const configKey = this.prefix ? key.slice(this.prefix.length) : key;

        if (!this.options.convertCase && !this.options.nestedDelimiter) {
          // Simple case - just keep the key as is
          config[configKey] = this.parseValue(value || '');
        } else {
          // Handle nested structure or case conversion
          this.setConfigValue(config, configKey, value || '');
        }
      }

      return config;
    } catch (error) {
      throw new ConfigLoaderError(`Failed to load environment variables: ${error}`, 'EnvLoader');
    }
  }

  private setConfigValue(config: Record<string, unknown>, key: string, value: string): void {
    try {
      // Handle provider-specific environment variables (only for application usage, not tests)
      if (!this.prefix && !this.options.convertCase) {
        const providerMapping = this.getProviderMapping(key);
        if (providerMapping) {
          // For certain fields, we need to preserve the string value
          const shouldPreserveString = this.shouldPreserveStringForKey(key);
          const parsedValue = shouldPreserveString ? value : this.parseValue(value);
          this.setNestedValue(config, providerMapping.path, parsedValue);
          return;
        }
      }

      const parsedValue = this.parseValue(value);

      if (this.options.convertCase) {
        // Convert to camelCase
        const camelKey = this.toCamelCase(key);
        config[camelKey] = parsedValue;
      } else if (
        this.options.nestedDelimiter &&
        this.options.nestedDelimiter !== '_' &&
        key.includes(this.options.nestedDelimiter)
      ) {
        // Handle nested delimiter (e.g., APP__NAME -> { APP: { NAME: value } })
        const parts = key.split(this.options.nestedDelimiter);
        this.setNestedValue(config, parts, parsedValue);
      } else {
        // Convert to nested structure based on underscores, or keep as-is if no underscores
        if (key.includes('_')) {
          const path = key.toLowerCase().split('_');
          this.setNestedValue(config, path, parsedValue);
        } else {
          // Single key without underscores - keep original case
          config[key] = parsedValue;
        }
      }
    } catch {
      // Skip environment variables that can't be set (readonly properties)
      // This is expected behavior for system environment variables
    }
  }

  private setNestedValue(obj: Record<string, unknown>, path: string[], value: unknown): boolean {
    if (path.length === 0) {
      return false; // Cannot set value on empty path
    }
    const lastKey = path.pop();
    if (!lastKey) {
      return false; // This should never happen due to length check above
    }

    try {
      const target = path.reduce((acc, key) => {
        if (!acc[key] || typeof acc[key] !== 'object') {
          acc[key] = {};
        }
        return acc[key] as Record<string, unknown>;
      }, obj);

      (target as Record<string, unknown>)[lastKey] = value;
      return true;
    } catch {
      // If we can't assign to any property (readonly), skip this env var silently
      return false;
    }
  }

  private toCamelCase(str: string): string {
    return str.toLowerCase().replace(/_([a-z])/g, (_, char) => char.toUpperCase());
  }

  private shouldPreserveStringForKey(key: string): boolean {
    // Keys that should preserve string values even if they look like numbers
    const preserveStringKeys = ['QM_WEBMASTER_ID', 'IB_MARKET_DATA_TYPE'];
    return preserveStringKeys.includes(key);
  }

  private getProviderMapping(envKey: string): { path: string[] } | null {
    // Provider-specific and special environment variable mappings
    const providerMappings: Record<string, string[]> = {
      // WebShare provider mappings
      WEBSHARE_API_KEY: ['webshare', 'apiKey'],
      WEBSHARE_API_URL: ['webshare', 'apiUrl'],
      WEBSHARE_ENABLED: ['webshare', 'enabled'],

      // EOD provider mappings
      EOD_API_KEY: ['providers', 'eod', 'apiKey'],
      EOD_BASE_URL: ['providers', 'eod', 'baseUrl'],
      EOD_TIER: ['providers', 'eod', 'tier'],
      EOD_ENABLED: ['providers', 'eod', 'enabled'],
      EOD_PRIORITY: ['providers', 'eod', 'priority'],

      // Interactive Brokers provider mappings
      IB_GATEWAY_HOST: ['providers', 'ib', 'gateway', 'host'],
      IB_GATEWAY_PORT: ['providers', 'ib', 'gateway', 'port'],
      IB_CLIENT_ID: ['providers', 'ib', 'gateway', 'clientId'],
      IB_ACCOUNT: ['providers', 'ib', 'account'],
      IB_MARKET_DATA_TYPE: ['providers', 'ib', 'marketDataType'],
      IB_ENABLED: ['providers', 'ib', 'enabled'],
      IB_PRIORITY: ['providers', 'ib', 'priority'],

      // QuoteMedia provider mappings
      QM_USERNAME: ['providers', 'qm', 'username'],
      QM_PASSWORD: ['providers', 'qm', 'password'],
      QM_BASE_URL: ['providers', 'qm', 'baseUrl'],
      QM_WEBMASTER_ID: ['providers', 'qm', 'webmasterId'],
      QM_ENABLED: ['providers', 'qm', 'enabled'],
      QM_PRIORITY: ['providers', 'qm', 'priority'],

      // Yahoo Finance provider mappings
      YAHOO_BASE_URL: ['providers', 'yahoo', 'baseUrl'],
      YAHOO_COOKIE_JAR: ['providers', 'yahoo', 'cookieJar'],
      YAHOO_CRUMB: ['providers', 'yahoo', 'crumb'],
      YAHOO_ENABLED: ['providers', 'yahoo', 'enabled'],
      YAHOO_PRIORITY: ['providers', 'yahoo', 'priority'],

      // General application config mappings
      NAME: ['name'],
      VERSION: ['version'],

      // Log mappings (using LOG_ prefix for all)
      LOG_LEVEL: ['log', 'level'],
      LOG_FORMAT: ['log', 'format'],
      LOG_HIDE_OBJECT: ['log', 'hideObject'],
      LOG_LOKI_ENABLED: ['log', 'loki', 'enabled'],
      LOG_LOKI_HOST: ['log', 'loki', 'host'],
      LOG_LOKI_PORT: ['log', 'loki', 'port'],

      // Special mappings to avoid conflicts
      DEBUG_MODE: ['debug'],
    };

    const mapping = providerMappings[envKey];
    return mapping ? { path: mapping } : null;
  }

  private parseValue(value: string): unknown {
    if (!this.options.parseValues && !this.options.parseJson) {
      return value;
    }

    // Try to parse as JSON first if enabled
    if (this.options.parseJson) {
      try {
        return JSON.parse(value);
      } catch {
        // Not JSON, continue with other parsing
      }
    }

    if (!this.options.parseValues) {
      return value;
    }

    // Handle booleans
    if (value.toLowerCase() === 'true') {
      return true;
    }
    if (value.toLowerCase() === 'false') {
      return false;
    }

    // Handle numbers (but preserve strings with leading zeros or plus signs)
    if (!/^[+-]/.test(value) && !/^0\d/.test(value)) {
      const num = Number(value);
      if (!isNaN(num) && value !== '') {
        return num;
      }
    }

    // Handle null/undefined
    if (value.toLowerCase() === 'null') {
      return null;
    }
    if (value.toLowerCase() === 'undefined') {
      return undefined;
    }

    // Return as string
    return value;
  }

  private loadEnvFile(filePath: string): void {
    try {
      const envContent = readFileSync(filePath, 'utf-8');
      const lines = envContent.split('\n');

      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith('#')) {
          continue; // Skip empty lines and comments
        }

        const equalIndex = trimmed.indexOf('=');
        if (equalIndex === -1) {
          continue; // Skip lines without =
        }

        const key = trimmed.substring(0, equalIndex).trim();
        let value = trimmed.substring(equalIndex + 1).trim();

        // Remove surrounding quotes if present
        if (
          (value.startsWith('"') && value.endsWith('"')) ||
          (value.startsWith("'") && value.endsWith("'"))
        ) {
          value = value.slice(1, -1);
        }

        // Only set if not already set (allows override precedence)
        if (!(key in process.env)) {
          process.env[key] = value;
        }
      }
    } catch (error: unknown) {
      // File not found is not an error (env files are optional)
      if (error && typeof error === 'object' && 'code' in error && error.code !== 'ENOENT') {
        // eslint-disable-next-line no-console
        console.warn(
          `Warning: Could not load env file ${filePath}:`,
          error instanceof Error ? error.message : String(error)
        );
      }
    }
  }
}
