import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Optional,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { errorResponse } from '../responses/api-response';
import { ErrorAlertService } from '../../modules/mailer/error-alert.service';

/**
 * Global exception filter.
 * Per nest.md: errors return { success: false, message, data: null, extra_data: [] }
 * Never exposes stack traces or internal errors.
 *
 * Also humanizes class-validator errors:
 *   "short_code should not be empty" → "Short Code is required"
 *   "name must be shorter than or equal to 255 characters" → "Name must be at most 255 characters"
 */
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  // Optional so the filter still works when constructed by hand (specs,
  // or a bootstrap that predates the APP_FILTER registration).
  constructor(@Optional() private readonly errorAlerts?: ErrorAlertService) {}

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Internal server error';

    // Log non-HTTP errors for debugging
    if (!(exception instanceof HttpException)) {
      console.error('[GlobalExceptionFilter] Unhandled error:', exception);
    }

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const exResponse = exception.getResponse();

      if (typeof exResponse === 'string') {
        message = exResponse;
      } else if (typeof exResponse === 'object' && exResponse !== null) {
        const resp = exResponse as Record<string, unknown>;
        if (Array.isArray(resp.message)) {
          // Log validation rejections with method + URL so the
          // forbidNonWhitelisted hardening (commit 84b9f9e) can be
          // tracked back to the offending FE call. Helps narrow
          // down which DTO is missing a field. Remove once every
          // FE→BE field drift is patched.

          console.warn(
            '[Validation 400]',
            ctx.getRequest<Request>().method,
            ctx.getRequest<Request>().url,
            '→',
            (resp.message as string[]).join(' | '),
          );
          message = humanizeValidationMessages(resp.message as string[]);
        } else if (typeof resp.message === 'string') {
          message = resp.message;
        }
      }
    }

    // Email the dev team on 5xx only. The response body still carries
    // the generic message — nothing extra leaks to the caller.
    if (status >= HttpStatus.INTERNAL_SERVER_ERROR) {
      this.alertDevelopers(ctx.getRequest<Request>(), status, exception);
    }

    response.status(status).json(errorResponse(message));
  }

  private alertDevelopers(request: Request, status: number, exception: unknown): void {
    if (!this.errorAlerts) return;
    try {
      const err = exception instanceof Error ? exception : null;
      const header = (name: string): string | null => {
        const v = request.headers[name];
        return (Array.isArray(v) ? v[0] : v) || null;
      };
      this.errorAlerts.notify({
        method: request.method,
        url: request.originalUrl || request.url,
        statusCode: status,
        message: err?.message || String(exception),
        stack: err?.stack ?? null,
        tenantId: header('x-tenant-id'),
        // Set by JwtStrategy on authenticated routes.
        userId: (request.user as { id?: string } | undefined)?.id ?? null,
        requestId: header('x-request-id'),
      });
    } catch {
      // Alerting is best-effort — never let it change the response.
    }
  }
}

/** Convert snake_case / camelCase field names to Title Case. */
function humanizeFieldName(raw: string): string {
  return raw
    .replace(/_/g, ' ')
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/\b\w/g, (c) => c.toUpperCase());
}

/** Convert a single class-validator message to user-friendly form. */
function humanizeOne(msg: string): string {
  // class-validator messages always start with the property name
  const firstWord = msg.split(/\s+/)[0];
  const label = humanizeFieldName(firstWord);
  const rest = msg.slice(firstWord.length).trim();

  // Common patterns
  if (/should not be empty|must not be empty/i.test(rest)) return `${label} is required`;
  if (/must be a valid (\S+)/i.test(rest)) {
    const type = rest.match(/must be a valid (\S+)/i)![1].replace(/[^a-zA-Z]/g, '');
    return `${label} must be a valid ${type}`;
  }
  if (/must be an email/i.test(rest)) return `${label} must be a valid email`;
  if (/must be a uuid/i.test(rest)) return `${label} must be a valid ID`;
  if (/must be a boolean/i.test(rest)) return `${label} must be true or false`;
  if (/must be a number/i.test(rest)) return `${label} must be a number`;
  if (/must be a string/i.test(rest)) return `${label} must be text`;
  if (/must be shorter than or equal to (\d+)/i.test(rest)) {
    const max = rest.match(/(\d+)/)![1];
    return `${label} must be at most ${max} characters`;
  }
  if (/must be longer than or equal to (\d+)/i.test(rest)) {
    const min = rest.match(/(\d+)/)![1];
    return `${label} must be at least ${min} characters`;
  }
  if (/must not be less than (\d+)/i.test(rest)) {
    const min = rest.match(/(\d+)/)![1];
    return `${label} must be at least ${min}`;
  }
  if (/must not be greater than (\d+)/i.test(rest)) {
    const max = rest.match(/(\d+)/)![1];
    return `${label} must be at most ${max}`;
  }
  if (/must match .+ regular expression/i.test(rest)) return `${label} has an invalid format`;
  if (/must be one of the following values/i.test(rest)) return `${label} has an invalid value`;
  if (/must be a valid iso 8601 date/i.test(rest)) return `${label} must be a valid date`;

  // Fallback — use label + rest (still more readable than raw snake_case)
  return `${label} ${rest}`.trim();
}

/**
 * Takes the raw array of class-validator messages and returns a single
 * user-friendly sentence. Groups by field so multiple errors on the same
 * field don't spam the user.
 */
function humanizeValidationMessages(messages: string[]): string {
  const seen = new Map<string, string>();
  for (const m of messages) {
    const field = m.split(/\s+/)[0];
    if (!seen.has(field)) {
      // Prefer "required" messages over "type" errors when multiple exist
      const required = messages.find((x) => x.startsWith(field) && /should not be empty/.test(x));
      seen.set(field, humanizeOne(required || m));
    }
  }
  return Array.from(seen.values()).join('. ');
}
