import {
  Injectable,
  Logger,
  OnModuleInit,
  OnModuleDestroy,
} from '@nestjs/common';
import { DataSource } from 'typeorm';

/**
 * ─────────────────────────────────────────────────────────────────────────────
 *  DatabaseKeepAliveService
 *
 *  Pings every configured DataSource at a regular interval to:
 *    - Prevent MySQL `wait_timeout` from closing idle connections
 *    - Detect stale connections after WiFi network switches (office → home)
 *    - Auto-reconnect if a connection was silently dropped
 *    - Keep connections warm across laptop sleep/wake cycles
 *
 *  Why this matters:
 *    - MySQL closes idle connections after `wait_timeout` (default 8 hours)
 *    - But network switches / sleep can drop connections in seconds
 *    - Without this, first request after network change = ECONNRESET error
 * ─────────────────────────────────────────────────────────────────────────────
 */
@Injectable()
export class DatabaseKeepAliveService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(DatabaseKeepAliveService.name);
  private intervalHandle: NodeJS.Timeout | null = null;

  // Ping every 30 seconds — aggressive enough to detect dropped connections fast
  private readonly PING_INTERVAL_MS = 30 * 1000;

  constructor(private readonly dataSources: DataSource[]) {}

  onModuleInit() {
    this.logger.log(
      `🛡️  Keep-alive starting for ${this.dataSources.length} database(s)`,
    );

    // Run once immediately on startup
    this.pingAll();

    // Then run every 30s
    this.intervalHandle = setInterval(() => {
      this.pingAll();
    }, this.PING_INTERVAL_MS);
  }

  onModuleDestroy() {
    if (this.intervalHandle) {
      clearInterval(this.intervalHandle);
      this.intervalHandle = null;
    }
  }

  /**
   * Ping all databases in parallel.
   * Each ping is independent — one DB down doesn't affect the others.
   */
  private async pingAll(): Promise<void> {
    const results = await Promise.allSettled(
      this.dataSources.map((ds) => this.pingOne(ds)),
    );

    const failed = results.filter((r) => r.status === 'rejected');
    if (failed.length > 0) {
      this.logger.warn(
        `⚠️  ${failed.length}/${this.dataSources.length} DB ping(s) failed — connections may be re-establishing`,
      );
    }
  }

  /**
   * Ping a single DataSource and try to recover if it's disconnected.
   */
  private async pingOne(ds: DataSource): Promise<void> {
    const dbName = (ds.options as any).name || (ds.options as any).database || 'unknown';

    try {
      // If not initialized, try to initialize
      if (!ds.isInitialized) {
        this.logger.warn(`🔄 [${dbName}] Not initialized — attempting connect...`);
        await ds.initialize();
        this.logger.log(`✅ [${dbName}] Reconnected successfully`);
        return;
      }

      // Simple ping query — fast and safe
      await ds.query('SELECT 1');
    } catch (error: any) {
      const msg = error?.message || String(error);
      this.logger.warn(`⚠️  [${dbName}] Ping failed: ${msg}`);

      // Try to recover dead connections
      if (
        msg.includes('ECONNRESET') ||
        msg.includes('PROTOCOL_CONNECTION_LOST') ||
        msg.includes('ETIMEDOUT') ||
        msg.includes('ENOTFOUND') ||
        msg.includes('ECONNREFUSED')
      ) {
        await this.tryReconnect(ds, dbName);
      }
    }
  }

  /**
   * Destroy and re-initialize a broken DataSource.
   * This is what handles the "WiFi switched → old TCP dead" case.
   */
  private async tryReconnect(ds: DataSource, dbName: string): Promise<void> {
    try {
      this.logger.warn(`🔄 [${dbName}] Attempting to recover connection...`);

      if (ds.isInitialized) {
        try {
          await ds.destroy();
        } catch {
          // ignore — was probably already broken
        }
      }

      await ds.initialize();
      this.logger.log(`✅ [${dbName}] Connection recovered`);
    } catch (error: any) {
      this.logger.error(
        `❌ [${dbName}] Recovery failed: ${error?.message || error}`,
      );
    }
  }
}