import { join } from 'path';
import { z } from 'zod';
import { EnvLoader } from './loaders/env.loader';
import { FileLoader } from './loaders/file.loader';
import { ConfigError, ConfigValidationError } from './errors';
import { getLogger } from '@stock-bot/logger';
import type {
  ConfigLoader,
  ConfigManagerOptions,
  ConfigSchema,
  DeepPartial,
  Environment,
} from './types';

export class ConfigManager<T = Record<string, unknown>> {
  private readonly logger = getLogger('config-manager');
  private config: T | null = null;
  private loaders: ConfigLoader[];
  private environment: Environment;
  private schema?: ConfigSchema;

  constructor(options: ConfigManagerOptions = {}) {
    this.environment = options.environment || this.detectEnvironment();

    // Default loaders if none provided
    if (options.loaders) {
      this.loaders = options.loaders;
    } else {
      const configPath = options.configPath || join(process.cwd(), 'config');
      this.loaders = [
        new FileLoader(configPath, this.environment),
        new EnvLoader(''), // No prefix for env vars to match our .env file
      ];
    }
  }

  /**
   * Initialize the configuration by loading from all sources synchronously.
   */
  initialize(schema?: ConfigSchema): T {
    if (this.config) {
      return this.config;
    }

    this.schema = schema;

    // Sort loaders by priority (higher priority last)
    const sortedLoaders = [...this.loaders].sort((a, b) => a.priority - b.priority);

    // Load configurations from all sources
    const configs: Record<string, unknown>[] = [];
    for (const loader of sortedLoaders) {
      // Assuming all loaders now have a synchronous `load` method
      const config = loader.load();
      if (config && Object.keys(config).length > 0) {
        configs.push(config);
      }
    }

    // Merge all configurations
    const mergedConfig = this.deepMerge(...configs) as T;

    // Add environment if not present
    if (
      typeof mergedConfig === 'object' &&
      mergedConfig !== null &&
      !('environment' in mergedConfig)
    ) {
      (mergedConfig as Record<string, unknown>)['environment'] = this.environment;
    }

    // Validate if schema provided
    if (this.schema) {
      try {
        this.config = this.schema.parse(mergedConfig) as T;
      } catch (error) {
        if (error instanceof z.ZodError) {
          const errorDetails = error.errors.map(err => ({
            path: err.path.join('.'),
            message: err.message,
            code: err.code,
            expected: (err as any).expected,
            received: (err as any).received,
          }));
          
          this.logger.error('Configuration validation failed:', errorDetails);
          
          throw new ConfigValidationError('Configuration validation failed', error.errors);
        }
        throw error;
      }
    } else {
      this.config = mergedConfig;
    }

    return this.config;
  }

  /**
   * Get the current configuration
   */
  get(): T {
    if (!this.config) {
      throw new ConfigError('Configuration not initialized. Call initialize() first.');
    }
    return this.config;
  }

  /**
   * Get a specific configuration value by path
   */
  getValue<R = unknown>(path: string): R {
    const config = this.get();
    const keys = path.split('.');
    let value: unknown = config;

    for (const key of keys) {
      if (value && typeof value === 'object' && key in value) {
        value = (value as Record<string, unknown>)[key];
      } else {
        throw new ConfigError(`Configuration key not found: ${path}`);
      }
    }

    return value as R;
  }

  /**
   * Check if a configuration path exists
   */
  has(path: string): boolean {
    try {
      this.getValue(path);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Update configuration at runtime (useful for testing)
   */
  set(updates: DeepPartial<T>): void {
    if (!this.config) {
      throw new ConfigError('Configuration not initialized. Call initialize() first.');
    }

    const updated = this.deepMerge(
      this.config as Record<string, unknown>,
      updates as Record<string, unknown>
    ) as T;

    // Re-validate if schema is present
    if (this.schema) {
      try {
        this.config = this.schema.parse(updated) as T;
      } catch (error) {
        if (error instanceof z.ZodError) {
          throw new ConfigValidationError(
            'Configuration validation failed after update',
            error.errors
          );
        }
        throw error;
      }
    } else {
      this.config = updated;
    }
  }

  /**
   * Get the current environment
   */
  getEnvironment(): Environment {
    return this.environment;
  }

  /**
   * Reset configuration (useful for testing)
   */
  reset(): void {
    this.config = null;
  }

  /**
   * Validate configuration against a schema
   */
  validate<S extends ConfigSchema>(schema: S): z.infer<S> {
    const config = this.get();
    return schema.parse(config);
  }

  /**
   * Create a typed configuration getter
   */
  createTypedGetter<S extends z.ZodSchema>(schema: S): () => z.infer<S> {
    return () => this.validate(schema);
  }

  private detectEnvironment(): Environment {
    const env = process.env.NODE_ENV?.toLowerCase();
    switch (env) {
      case 'production':
      case 'prod':
        return 'production';
      case 'test':
        return 'test';
      case 'development':
      case 'dev':
      default:
        return 'development';
    }
  }

  private deepMerge(...objects: Record<string, unknown>[]): Record<string, unknown> {
    const result: Record<string, unknown> = {};

    for (const obj of objects) {
      for (const [key, value] of Object.entries(obj)) {
        if (value === null || value === undefined) {
          result[key] = value;
        } else if (
          typeof value === 'object' &&
          !Array.isArray(value) &&
          !(value instanceof Date) &&
          !(value instanceof RegExp)
        ) {
          result[key] = this.deepMerge(
            (result[key] as Record<string, unknown>) || ({} as Record<string, unknown>),
            value as Record<string, unknown>
          );
        } else {
          result[key] = value;
        }
      }
    }

    return result;
  }
}
