import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { QueryFailedError } from 'typeorm';
import { ZodError } from 'zod';
import { logger } from '..';

@Catch()
class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let errorDetails: any = 'An unexpected error occurred';
    let errorMessage: string | null = null;

    logger.error(`Error eee: ${exception}`);

    const normalizeMessage = (v: any) => {
      try {
        if (typeof v === 'string') return v.replace(/\s+/g, ' ').trim();
        if (v === undefined || v === null) return '';
        if (v.driverError?.sqlMessage) return String(v.driverError.sqlMessage);
        if (v.sqlMessage) return String(v.sqlMessage);
        if (v.message && typeof v.message === 'string')
          return v.message.replace(/\s+/g, ' ').trim();
        if (v.message && Array.isArray(v.message)) return v.message.join(', ');
        return JSON.stringify(v);
      } catch {
        return String(v);
      }
    };

    const extractUniqueConstraintMessage = (v: any): string | null => {
      const normalized = normalizeMessage(v);
      if (!normalized) return null;

      const directMatch = normalized.match(
        /Unique constraint failed on the constraint:\s*`[^`]+`/i,
      );
      if (directMatch?.[0]) return directMatch[0];

      const fieldsMatch = normalized.match(
        /Unique constraint failed on the fields:\s*\(([^)]+)\)/i,
      );
      if (fieldsMatch?.[1]) {
        const field = String(fieldsMatch[1])
          .replace(/["'`\s]/g, '')
          .split(',')[0];
        return field
          ? `Unique constraint failed on the constraint: \`${field}_key\``
          : 'Unique constraint failed on the constraint';
      }

      return null;
    };

    const isZodTree = (v: any) =>
      v && typeof v === 'object' && 'errors' in v && 'properties' in v;

    const collectZodTreeErrors = (node: any, prefix = ''): string[] => {
      if (!node || typeof node !== 'object') return [];
      const messages: string[] = [];
      const errors = Array.isArray(node.errors) ? node.errors : [];
      for (const message of errors) {
        messages.push(prefix ? `${prefix} : ${message}` : String(message));
      }
      const properties = node.properties ?? {};
      for (const key of Object.keys(properties)) {
        const nextPrefix = prefix ? `${prefix}.${key}` : key;
        messages.push(...collectZodTreeErrors(properties[key], nextPrefix));
      }
      return messages;
    };

    const groupZodTreeErrors = (
      node: any,
      prefix = '',
    ): Record<string, string[]> => {
      if (!node || typeof node !== 'object') return {};
      const grouped: Record<string, string[]> = {};
      const errors = Array.isArray(node.errors) ? node.errors : [];

      // Only add errors if there's a field name (skip root-level errors)
      for (const message of errors) {
        if (prefix) {
          if (!grouped[prefix]) {
            grouped[prefix] = [];
          }
          grouped[prefix].push(String(message));
        }
      }

      const properties = node.properties ?? {};
      for (const key of Object.keys(properties)) {
        const nextPrefix = prefix ? `${prefix}.${key}` : key;
        const nestedGrouped = groupZodTreeErrors(properties[key], nextPrefix);
        Object.assign(grouped, nestedGrouped);
      }
      return grouped;
    };

    // Handle HttpException
    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const res = exception.getResponse();
      const raw =
        typeof res === 'string' ? res : ((res as any)?.message ?? res);

      // Check if response already has the Zod tree structure
      if ((res as any)?.message && isZodTree((res as any).message)) {
        const messageObj = (res as any).message;
        // Convert Zod tree to grouped format for consistency
        const groupedErrors = groupZodTreeErrors(messageObj);
        errorDetails = {
          errors: groupedErrors,
        };
        const detailMessages: string[] = [];
        Object.entries(groupedErrors).forEach(([field, messages]) => {
          messages.forEach((msg) => {
            detailMessages.push(msg);
          });
        });
        errorMessage = detailMessages.join(', ');
      } else if (isZodTree(raw)) {
        const groupedErrors = groupZodTreeErrors(raw);
        errorDetails = {
          errors: groupedErrors,
        };
        const detailMessages: string[] = [];
        Object.entries(groupedErrors).forEach(([field, messages]) => {
          messages.forEach((msg) => {
            detailMessages.push(msg);
          });
        });
        errorMessage = detailMessages.join(', ');
      } else {
        const uniqueConstraintMessage = extractUniqueConstraintMessage(raw);
        if (uniqueConstraintMessage) {
          status = HttpStatus.BAD_REQUEST;
          errorDetails = uniqueConstraintMessage;
          errorMessage = null;
        } else {
          errorDetails = normalizeMessage(raw) || errorDetails;
        }
      }
    }
    // Handle ZodError
    else if (exception instanceof ZodError) {
      status = HttpStatus.BAD_REQUEST;
      const issueList = exception.issues.map((issue) => ({
        path: issue.path.join('.'),
        message: issue.message,
      }));

      // Group errors by path
      const groupedErrors = issueList.reduce(
        (acc, issue) => {
          if (!acc[issue.path]) {
            acc[issue.path] = [];
          }
          acc[issue.path].push(issue.message);
          return acc;
        },
        {} as Record<string, string[]>,
      );

      errorDetails = { errors: groupedErrors };
      errorMessage = issueList
        .map((issue) => `${issue.path} : ${issue.message}`)
        .join(', ');
    }
    // Handle generic Error (MySQL or JS errors) and ensure `message` is a single-line string
    else if (exception && typeof exception === 'object') {
      const ex: any = exception;

      if (
        ex instanceof QueryFailedError &&
        ((ex as any).driverError?.code === 'ER_DUP_ENTRY' ||
          (ex as any).driverError?.errno === 1062)
      ) {
        status = HttpStatus.BAD_REQUEST;
        const duplicateMessage = (ex as any).driverError?.sqlMessage;
        errorDetails = duplicateMessage
          ? duplicateMessage
          : 'Unique constraint failed on the constraint';
        errorMessage = null;
      } else {
        const uniqueConstraintMessage = extractUniqueConstraintMessage(ex);
        if (uniqueConstraintMessage) {
          status = HttpStatus.BAD_REQUEST;
          errorDetails = uniqueConstraintMessage;
          errorMessage = null;
        } else {
          const extractRaw = (): any => {
            if (!ex) return undefined;
            if (ex.driverError?.sqlMessage) return ex.driverError.sqlMessage;
            if (ex.sqlMessage) return ex.sqlMessage;
            if (typeof ex.message === 'string') return ex.message;
            if (ex.message && typeof ex.message === 'object') {
              return ex.message.sqlMessage ?? ex.message.message ?? ex.message;
            }
            return ex;
          };

          const stringify = (v: any) => {
            try {
              if (typeof v === 'string') return v;
              if (v === undefined || v === null) return '';
              if (typeof v === 'object') return JSON.stringify(v);
              return String(v);
            } catch {
              return String(v);
            }
          };

          const raw = extractRaw();
          errorDetails = stringify(raw) || 'An unexpected error occurred';

          // Collapse whitespace/newlines into a single space for one-line message
          errorDetails = errorDetails.replace(/\s+/g, ' ').trim();

          // For SQL/duplicate errors prefer BAD_REQUEST
          if (
            ex?.driverError?.sqlMessage ||
            ex?.sqlMessage ||
            /duplicate entry/i.test(errorDetails)
          ) {
            status = HttpStatus.BAD_REQUEST;
          }
        }
      }
    }

    logger.error(`Error message: ${normalizeMessage(errorDetails)}`);

    response.status(status).json({
      status,
      message: errorMessage,
      details: errorDetails,
      data: null,
    });
  }
}

export default GlobalExceptionFilter;
