import { Injectable, Logger, type OnModuleInit } from '@nestjs/common';
import { MailerService, type MailRecipient } from './mailer.service';
import { errorAlertTemplate } from './templates/error-alert.template';

/**
 * ErrorAlertService — emails the dev team when a request blows up with
 * a 500. Wired from GlobalExceptionFilter.
 *
 * Env:
 *   DEVELOPER_EMAILS — comma-separated recipients. Empty/unset turns
 *                      alerting off (local dev stays quiet).
 *   BREVO_API_KEY    — reused from MailerService; no key, no alerts.
 *
 * Never throws and never awaits from the caller's path: a failing
 * mailer must not turn a 500 into a hung request.
 *
 * Two throttles keep a crash loop from draining the Brevo quota (300
 * emails/day on the free tier):
 *   - the same error signature is muted for MUTE_MS after it alerts,
 *     and the suppressed count rides along on the next email;
 *   - a hard ceiling of MAX_PER_HOUR alerts across all signatures.
 */

export interface ErrorAlertInput {
  method: string;
  url: string;
  statusCode: number;
  message: string;
  stack: string | null;
  tenantId: string | null;
  userId: string | null;
  requestId: string | null;
}

const MUTE_MS = 10 * 60 * 1000;
const HOUR_MS = 60 * 60 * 1000;
const MAX_PER_HOUR = 20;

@Injectable()
export class ErrorAlertService implements OnModuleInit {
  private readonly logger = new Logger(ErrorAlertService.name);
  /** signature → { mutedUntil, suppressed } */
  private readonly muted = new Map<string, { mutedUntil: number; suppressed: number }>();
  /** Epoch ms of each alert sent in the trailing hour. */
  private sentTimestamps: number[] = [];

  constructor(private readonly mailer: MailerService) {}

  private get recipients(): MailRecipient[] {
    return (process.env.DEVELOPER_EMAILS || '')
      .split(',')
      .map((e) => e.trim())
      .filter((e) => e.includes('@'))
      .map((email) => ({ email }));
  }

  isEnabled(): boolean {
    return this.mailer.isConfigured() && this.recipients.length > 0;
  }

  /**
   * Say at boot whether alerting is live. Editing .env does NOT reach a
   * running process — without this line a misconfigured (or simply
   * not-yet-restarted) server looks identical to a working one, because
   * notify() skips in silence.
   */
  onModuleInit(): void {
    const count = this.recipients.length;
    if (this.isEnabled()) {
      this.logger.log(`500-error alerts enabled — ${count} recipient(s)`);
      return;
    }
    const missing = [
      this.mailer.isConfigured() ? null : 'BREVO_API_KEY',
      count > 0 ? null : 'DEVELOPER_EMAILS',
    ].filter(Boolean);
    this.logger.warn(`500-error alerts disabled — missing ${missing.join(' + ')}`);
  }

  /**
   * Fire-and-forget. Returns immediately; the send happens on a
   * detached promise whose rejections are swallowed into the log.
   */
  notify(input: ErrorAlertInput): void {
    if (!this.isEnabled()) return;
    const now = Date.now();
    const signature = this.signature(input);
    const entry = this.muted.get(signature);
    if (entry && entry.mutedUntil > now) {
      entry.suppressed += 1;
      this.logger.debug(`Muted repeat (${entry.suppressed}) of ${signature}`);
      return;
    }
    this.sentTimestamps = this.sentTimestamps.filter((t) => now - t < HOUR_MS);
    if (this.sentTimestamps.length >= MAX_PER_HOUR) {
      this.logger.warn(`Alert ceiling reached (${MAX_PER_HOUR}/hour) — dropping ${signature}`);
      return;
    }
    const repeatCount = (entry?.suppressed ?? 0) + 1;
    this.muted.set(signature, { mutedUntil: now + MUTE_MS, suppressed: 0 });
    this.sentTimestamps.push(now);
    this.prune(now);

    const { subject, html, text } = errorAlertTemplate({
      env: process.env.APP_ENV || 'development',
      occurredAt: new Date(now).toISOString(),
      repeatCount,
      ...input,
    });
    
    void this.mailer
      .send({ to: this.recipients, subject, html, text })
      .catch((err: Error) => this.logger.error(`Error-alert email failed: ${err.message}`));
  }

  /**
   * Collapse ids out of the path so `/queries/<uuid-a>` and
   * `/queries/<uuid-b>` failing the same way count as one error
   * rather than two, and the mute window actually bites.
   */
  private signature(input: ErrorAlertInput): string {
    const path = input.url
      .split('?')[0]
      .replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '/:id')
      .replace(/\/\d+/g, '/:n');
    return `${input.method} ${path} :: ${input.message}`;
  }

  /** Drop mute entries that have expired so the map stays bounded. */
  private prune(now: number): void {
    for (const [key, value] of this.muted) {
      if (value.mutedUntil <= now) this.muted.delete(key);
    }
  }
}
