import { randomUUID } from 'crypto';
import { createWriteStream, WriteStream } from 'fs';
import { mkdir } from 'fs/promises';
import { join } from 'path';

export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace';
export type LogContext = Record<string, any>;

export interface LogEntry {
  timestamp: string;
  level: LogLevel;
  message: string;
  context?: LogContext;
  correlationId?: string;
  service?: string;
  component?: string;
  error?: {
    name: string;
    message: string;
    stack?: string;
  };
  metadata?: {
    pid: number;
    hostname: string;
    version: string;
  };
}

export interface LogTransport {
  name: string;
  write(entry: LogEntry): Promise<void>;
  flush?(): Promise<void>;
  close?(): Promise<void>;
}

/**
 * Console transport for logging to stdout/stderr
 */
export class ConsoleTransport implements LogTransport {
  public readonly name = 'console';
  private readonly colors: boolean;

  constructor(colors = true) {
    this.colors = colors;
  }

  public async write(entry: LogEntry): Promise<void> {
    const formatted = this.formatEntry(entry);

    if (entry.level === 'error' || entry.level === 'warn') {
      console.error(formatted);
    } else {
      console.log(formatted);
    }
  }

  private formatEntry(entry: LogEntry): string {
    const timestamp = entry.timestamp;
    const level = this.colors ? this.colorizeLevel(entry.level) : entry.level.toUpperCase();
    const service = entry.service ? `[${entry.service}]` : '';
    const component = entry.component ? `[${entry.component}]` : '';
    const correlationId = entry.correlationId ? `(${entry.correlationId.slice(0, 8)})` : '';

    let message = `${timestamp} ${level} ${service}${component}${correlationId} ${entry.message}`;

    if (entry.context && Object.keys(entry.context).length > 0) {
      message += ` | Context: ${JSON.stringify(entry.context)}`;
    }

    if (entry.error) {
      message += `\n  Error: ${entry.error.name}: ${entry.error.message}`;
      if (entry.error.stack) {
        message += `\n  Stack: ${entry.error.stack}`;
      }
    }

    return message;
  }

  private colorizeLevel(level: LogLevel): string {
    if (!this.colors) return level.toUpperCase();

    const colors = {
      error: '\x1b[31m', // Red
      warn: '\x1b[33m',  // Yellow
      info: '\x1b[36m',  // Cyan
      debug: '\x1b[35m', // Magenta
      trace: '\x1b[37m', // White
    };

    const reset = '\x1b[0m';
    return `${colors[level]}${level.toUpperCase()}${reset}`;
  }
}

/**
 * File transport for logging to files with rotation
 */
export class FileTransport implements LogTransport {
  public readonly name = 'file';
  private writeStream: WriteStream | null = null;
  private readonly logPath: string;
  private readonly maxFileSize: number;
  private readonly maxFiles: number;
  private currentFileSize = 0;

  constructor(
    logPath: string,
    maxFileSize = 10 * 1024 * 1024, // 10MB
    maxFiles = 5
  ) {
    this.logPath = logPath;
    this.maxFileSize = maxFileSize;
    this.maxFiles = maxFiles;
  }

  public async write(entry: LogEntry): Promise<void> {
    if (!this.writeStream) {
      await this.createWriteStream();
    }

    const formatted = JSON.stringify(entry) + '\n';

    // Check if we need to rotate the log file
    if (this.currentFileSize + formatted.length > this.maxFileSize) {
      await this.rotateLogFile();
    }

    return new Promise((resolve, reject) => {
      this.writeStream!.write(formatted, (error) => {
        if (error) {
          reject(error);
        } else {
          this.currentFileSize += formatted.length;
          resolve();
        }
      });
    });
  }

  public async flush(): Promise<void> {
    if (this.writeStream) {
      return new Promise((resolve) => {
        this.writeStream!.end(resolve);
      });
    }
  }

  public async close(): Promise<void> {
    if (this.writeStream) {
      await this.flush();
      this.writeStream = null;
    }
  }

  private async createWriteStream(): Promise<void> {
    // Ensure log directory exists
    await mkdir(join(this.logPath, '..'), { recursive: true });

    this.writeStream = createWriteStream(this.logPath, { flags: 'a' });
    this.currentFileSize = 0; // Reset size counter
  }

  private async rotateLogFile(): Promise<void> {
    if (this.writeStream) {
      await this.close();
    }

    // Rotate existing files
    for (let i = this.maxFiles - 1; i > 0; i--) {
      const oldFile = `${this.logPath}.${i}`;
      const newFile = `${this.logPath}.${i + 1}`;

      try {
        const fs = await import('fs/promises');
        await fs.rename(oldFile, newFile);
      } catch {
        // File doesn't exist, continue
      }
    }

    // Move current log to .1
    try {
      const fs = await import('fs/promises');
      await fs.rename(this.logPath, `${this.logPath}.1`);
    } catch {
      // File doesn't exist, continue
    }

    await this.createWriteStream();
  }
}

/**
 * Enterprise-grade structured logger with multiple transports and correlation tracking
 */
export class Logger {
  private static instance: Logger;
  private transports: Map<string, LogTransport> = new Map();
  private logLevel: LogLevel;
  private correlationId: string | null = null;
  private service: string | null = null;
  private component: string | null = null;

  private readonly logLevels: Record<LogLevel, number> = {
    error: 0,
    warn: 1,
    info: 2,
    debug: 3,
    trace: 4,
  };

  private constructor() {
    this.logLevel = (process.env.LOG_LEVEL as LogLevel) || 'info';
    this.initializeTransports();
  }

  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  private async initializeTransports(): Promise<void> {
    // Add console transport
    this.addTransport(new ConsoleTransport(process.env.LOG_COLORS !== 'false'));

    // Add file transport if log directory is specified
    const logDir = process.env.LOG_DIRECTORY || './logs';
    const logFile = join(logDir, 'application.log');
    this.addTransport(new FileTransport(logFile));
  }

  public setCorrelationId(correlationId: string): void {
    this.correlationId = correlationId;
  }

  public setService(service: string): void {
    this.service = service;
  }

  public setComponent(component: string): void {
    this.component = component;
  }

  public clearContext(): void {
    this.correlationId = null;
    this.service = null;
    this.component = null;
  }

  public child(context: { service?: string; component?: string; correlationId?: string }): Logger {
    const childLogger = Object.create(this);
    childLogger.service = context.service || this.service;
    childLogger.component = context.component || this.component;
    childLogger.correlationId = context.correlationId || this.correlationId;
    return childLogger;
  }

  public error(message: string, context?: LogContext, error?: Error): void {
    this.log('error', message, context, error);
  }

  public warn(message: string, context?: LogContext): void {
    this.log('warn', message, context);
  }

  public info(message: string, context?: LogContext): void {
    this.log('info', message, context);
  }

  public debug(message: string, context?: LogContext): void {
    this.log('debug', message, context);
  }

  public trace(message: string, context?: LogContext): void {
    this.log('trace', message, context);
  }

  public log(level: LogLevel, message: string, context?: LogContext, error?: Error): void {
    if (!this.shouldLog(level)) return;

    const entry: LogEntry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      context,
      correlationId: this.correlationId || undefined,
      service: this.service || undefined,
      component: this.component || undefined,
      error: error ? {
        name: error.name,
        message: error.message,
        stack: error.stack,
      } : undefined,
      metadata: {
        pid: process.pid,
        hostname: require('os').hostname(),
        version: process.version,
      },
    };

    // Write to all transports (fire and forget for performance)
    this.writeToTransports(entry).catch(console.error);
  }

  private shouldLog(level: LogLevel): boolean {
    return this.logLevels[level] <= this.logLevels[this.logLevel];
  }

  private async writeToTransports(entry: LogEntry): Promise<void> {
    const writePromises = Array.from(this.transports.values()).map(transport =>
      transport.write(entry).catch(error => {
        console.error(`Failed to write to transport ${transport.name}:`, error);
      })
    );

    await Promise.allSettled(writePromises);
  }

  public addTransport(transport: LogTransport): void {
    this.transports.set(transport.name, transport);
  }

  public removeTransport(name: string): boolean {
    return this.transports.delete(name);
  }

  public getTransports(): LogTransport[] {
    return Array.from(this.transports.values());
  }

  public async flush(): Promise<void> {
    const flushPromises = Array.from(this.transports.values())
      .filter(transport => transport.flush)
      .map(transport => transport.flush!());

    await Promise.allSettled(flushPromises);
  }

  public async close(): Promise<void> {
    const closePromises = Array.from(this.transports.values())
      .filter(transport => transport.close)
      .map(transport => transport.close!());

    await Promise.allSettled(closePromises);
    this.transports.clear();
  }

  public async performance<T>(operation: string, fn: () => Promise<T>): Promise<T> {
    return this.performanceSync(operation, fn);
  }

  public async performanceSync<T>(operation: string, fn: () => Promise<T>): Promise<T> {
    const startTime = Date.now();
    const correlationId = this.generateCorrelationId();

    this.info(`Starting operation: ${operation}`, {
      operation,
      correlationId,
      startTime: new Date(startTime).toISOString()
    });

    try {
      const result = await fn();
      const duration = Date.now() - startTime;

      this.info(`Operation completed: ${operation}`, {
        operation,
        correlationId,
        duration,
        status: 'success'
      });

      return result;
    } catch (error) {
      const duration = Date.now() - startTime;

      this.error(`Operation failed: ${operation}`, {
        operation,
        correlationId,
        duration,
        status: 'error'
      }, error instanceof Error ? error : new Error(String(error)));

      throw error;
    }
  }

  public metric(name: string, value: number, tags?: Record<string, string>): void {
    this.debug(`Metric: ${name}`, { metric: name, value, tags });
  }

  public counter(name: string, increment = 1, tags?: Record<string, string>): void {
    this.metric(name, increment, { ...tags, type: 'counter' });
  }

  public gauge(name: string, value: number, tags?: Record<string, string>): void {
    this.metric(name, value, { ...tags, type: 'gauge' });
  }

  public histogram(name: string, value: number, tags?: Record<string, string>): void {
    this.metric(name, value, { ...tags, type: 'histogram' });
  }

  public audit(action: string, resource: string, context?: LogContext): void {
    this.info(`Audit: ${action}`, {
      audit: true,
      action,
      resource,
      ...context
    });
  }

  public security(event: string, details: LogContext): void {
    this.warn(`Security event: ${event}`, {
      security: true,
      event,
      ...details
    });
  }

  public health(service: string, status: 'healthy' | 'unhealthy' | 'degraded', details?: LogContext): void {
    const level = status === 'healthy' ? 'info' : 'warn';
    this.log(level, `Health check: ${service} is ${status}`, {
      health: true,
      service,
      status,
      ...details
    });
  }

  public static generateCorrelationId(): string {
    return randomUUID();
  }

  public generateCorrelationId(): string {
    return Logger.generateCorrelationId();
  }
}

// Export singleton instance for convenience
export default Logger.getInstance(); 