import type { ErrorRequestHandler, RequestHandler } from 'express';
import {
  BaseError as SequelizeBaseError,
  ValidationError as SequelizeValidationError,
  UniqueConstraintError,
} from 'sequelize';
import AppError from '@/shared/errors/app-error';
import NotFoundError from '@/shared/errors/not-found';
import { getNotification } from '@/shared/notifications/helpers';
import logger from '@/shared/logger';

/** The error body shape the frontend `ApiError` consumes. */
export interface ErrorResponseBody {
  message: string;
  code?: string;
  details?: unknown;
}

export interface NormalizedError {
  status: number;
  body: ErrorResponseBody;
  /** Unexpected errors (5xx) are logged; expected ones (4xx) are not. */
  unexpected: boolean;
}

function buildBody(
  message: string,
  code?: string,
  details?: unknown,
): ErrorResponseBody {
  return {
    message,
    ...(code !== undefined ? { code } : {}),
    ...(details !== undefined ? { details } : {}),
  };
}

/**
 * Maps any thrown value to an HTTP status + client-safe body. Pure (no I/O), so
 * it is unit-testable; the middleware below adds logging and the response.
 */
export function normalizeError(error: unknown): NormalizedError {
  if (error instanceof AppError) {
    return {
      status: error.status,
      body: buildBody(error.message, error.code, error.details),
      unexpected: error.status >= 500,
    };
  }

  // Sequelize validation / unique-constraint violations are client errors. The
  // per-field messages are developer-defined (safe to surface), unlike raw SQL.
  if (
    error instanceof SequelizeValidationError ||
    error instanceof UniqueConstraintError
  ) {
    const firstMessage = error.errors?.[0]?.message;
    return {
      status: 400,
      body: buildBody(
        firstMessage || getNotification('errors.validation.message'),
        'errors.validation.message',
      ),
      unexpected: false,
    };
  }

  // Any other Sequelize/DB error → generic 400 (never leak SQL or schema).
  if (error instanceof SequelizeBaseError) {
    return {
      status: 400,
      body: buildBody(
        getNotification('errors.validation.message'),
        'errors.validation.message',
      ),
      unexpected: false,
    };
  }

  // Unknown / native errors → generic 500 (never leak internals to the client).
  return {
    status: 500,
    body: buildBody(
      getNotification('errors.internal.message'),
      'errors.internal.message',
    ),
    unexpected: true,
  };
}

/** Terminal Express error middleware. Must be registered last. */
export const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
  if (res.headersSent) {
    return next(error);
  }

  const normalized = normalizeError(error);

  if (normalized.unexpected) {
    logger.error('Unhandled request error', error, {
      method: req.method,
      path: req.originalUrl,
    });
  }

  res.status(normalized.status).json(normalized.body);
};

/** Catches requests that matched no route and routes them to `errorHandler`. */
export const notFoundHandler: RequestHandler = (_req, _res, next) => {
  next(new NotFoundError());
};
