import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Request, Response } from 'express';
import { ZodError } from 'zod';
import { getRequestContext } from '@/common/context/request-context';

/** Normalises every error into the ApiError envelope defined in @wms/shared. */
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger('Http');

  catch(exception: unknown, host: ArgumentsHost) {
    const http = host.switchToHttp();
    const res = http.getResponse<Response>();
    const req = http.getRequest<Request>();
    const requestId = getRequestContext()?.requestId ?? (req.headers['x-request-id'] as string | undefined);

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let body: Record<string, unknown> = { statusCode: status, error: 'InternalServerError', message: 'Unexpected error' };

    if (exception instanceof ZodError) {
      status = HttpStatus.BAD_REQUEST;
      body = {
        statusCode: status,
        error: 'ValidationError',
        code: 'VALIDATION_ERROR',
        message: 'Request validation failed',
        details: exception.issues.map((i) => ({ path: i.path.join('.'), message: i.message })),
      };
    } else if (exception instanceof HttpException) {
      status = exception.getStatus();
      const r = exception.getResponse();
      body = typeof r === 'string' ? { statusCode: status, error: exception.name, message: r } : { statusCode: status, ...(r as object) };
    } else if (isPgError(pgCause(exception))) {
      // Map common Postgres errors to meaningful responses (drizzle wraps them in DrizzleQueryError.cause)
      const pg = pgCause(exception) as { code: string; constraint?: string; detail?: string; message?: string };
      if (pg.code === '23505') {
        status = HttpStatus.CONFLICT;
        body = { statusCode: status, error: 'Conflict', code: 'DUPLICATE', message: 'A record with the same unique key already exists', details: { constraint: pg.constraint } };
      } else if (pg.code === '23503') {
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        const crossTenant = /Cross-tenant reference rejected/.test(pg.message ?? '');
        body = { statusCode: status, error: 'UnprocessableEntity', code: crossTenant ? 'CROSS_TENANT_REFERENCE' : 'REFERENCE_VIOLATION', message: crossTenant ? 'Referenced record does not exist in this tenant' : 'Referenced record does not exist or is still referenced', details: { constraint: pg.constraint } };
      } else if (pg.code === '23514') {
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        body = { statusCode: status, error: 'UnprocessableEntity', code: 'CHECK_VIOLATION', message: 'Data integrity rule violated', details: { constraint: pg.constraint } };
      } else if (pg.code === '42501') {
        status = HttpStatus.FORBIDDEN;
        body = { statusCode: status, error: 'Forbidden', code: 'RLS_DENIED', message: 'Access denied by row level security' };
      }
    }

    if (status >= 500) {
      this.logger.error(`${req.method} ${req.url} -> ${status}`, exception instanceof Error ? exception.stack : String(exception));
    } else if (status >= 400) {
      this.logger.warn(`${req.method} ${req.url} -> ${status} ${(body as { code?: string }).code ?? ''}`);
    }

    res.status(status).json({ ...body, requestId });
  }
}

/** Drizzle wraps driver errors; the Postgres error with the SQLSTATE code sits in `cause`. */
function pgCause(e: unknown): unknown {
  let cur = e;
  for (let i = 0; i < 3 && cur && typeof cur === 'object'; i++) {
    if (isPgError(cur) && /^[0-9A-Z]{5}$/.test((cur as { code: string }).code)) return cur;
    cur = (cur as { cause?: unknown }).cause;
  }
  return e;
}

function isPgError(e: unknown): e is { code: string; constraint?: string; detail?: string } {
  return typeof e === 'object' && e !== null && 'code' in e && typeof (e as { code: unknown }).code === 'string';
}
