import { NextResponse } from 'next/server';
import { ZodError } from 'zod';

import { logger } from '@/lib/logger';
import { getRequestId } from '@/lib/request-context';
import { captureErrorEvent } from '@/lib/services/error-events.service';
import { ERROR_CODES, type ErrorCode } from '@/lib/error-codes';

export class AppError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code?: string,
  ) {
    super(message);
    this.name = 'AppError';
  }
}

/**
 * Throw site for any registered error code. Consolidates the
 * status + plain-text message + stable code into one constructor.
 *
 *   throw new CodedError('EXPENSES_RECEIPT_REQUIRED');
 *
 * Pass `details` for structured payload (e.g. zod validation issues),
 * or `internalMessage` for an admin-only string that lands in the
 * error_events row but is NEVER returned to the user (the user gets
 * the plain-text message from the registry).
 */
export class CodedError extends AppError {
  /** Optional structured details surfaced to the client. */
  public details?: unknown;
  /** Optional verbose message for admin logs only - never sent to client. */
  public internalMessage?: string;

  constructor(code: ErrorCode, opts: { details?: unknown; internalMessage?: string } = {}) {
    const def = ERROR_CODES[code];
    super(def.status, def.userMessage, code);
    this.name = 'CodedError';
    this.details = opts.details;
    this.internalMessage = opts.internalMessage;
  }
}

/**
 * Backwards-compat shims: these existing subclasses are still used in
 * lots of places; new throw sites should prefer `CodedError` so the
 * code surfaces in the registry.
 *
 * Messages have been rewritten to plain language (no internal jargon)
 * so the user-facing toast reads naturally even before a service is
 * migrated to a specific CodedError code.
 */
export class NotFoundError extends AppError {
  constructor(entity: string) {
    // Plain-text version of "X not found" - the registered code stays
    // generic until callers migrate to specific codes per entity.
    super(
      404,
      `We couldn't find that ${entity.toLowerCase()}. It may have been removed.`,
      'NOT_FOUND',
    );
  }
}

export class ForbiddenError extends AppError {
  constructor(
    message = "You don't have permission to do that. Ask an admin if you think you should.",
  ) {
    super(403, message, 'FORBIDDEN');
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = 'Please sign in to continue.') {
    super(401, message, 'UNAUTHORIZED');
  }
}

export class ValidationError extends AppError {
  constructor(
    message: string,
    public details?: Array<{ field: string; message: string }>,
  ) {
    super(400, message, 'VALIDATION_ERROR');
  }
}

export class ConflictError extends AppError {
  constructor(message: string) {
    super(409, message, 'CONFLICT');
  }
}

export class RateLimitError extends AppError {
  constructor(public retryAfter: number) {
    super(
      429,
      "You've done that a lot in a short time. Please wait a moment and try again.",
      'RATE_LIMITED',
    );
  }
}

/**
 * Converts any thrown value into a sanitised NextResponse.
 *
 * Always attaches the active `X-Request-Id` to:
 *   - the response header (so a curl/dev-tools user can see it)
 *   - the JSON body (so a UI toast can surface "Error ID: …")
 *
 * For unhandled (5xx) errors, also persists a row to `error_events`
 * so a super admin can paste the request id into the inspector and
 * pull the full stack + body excerpt + log lines.
 *
 * Never leaks stack traces, internal paths, or DB error details to
 * the client - that data goes to pino + the error_events row only.
 */
export function errorResponse(error: unknown): NextResponse {
  const requestId = getRequestId();
  const headers = requestId ? { 'X-Request-Id': requestId } : undefined;

  if (error instanceof AppError) {
    const body: Record<string, unknown> = {
      error: error.message,
      code: error.code,
    };
    if (requestId) body.requestId = requestId;
    if (error instanceof ValidationError && error.details) {
      body.details = error.details;
    }
    if (error instanceof CodedError && error.details !== undefined) {
      body.details = error.details;
    }
    if (error instanceof RateLimitError) {
      body.retryAfter = error.retryAfter;
    }
    // 4xx errors are user-action mistakes (validation, not-found,
    // permission). They DON'T go to error_events - that table is for
    // platform faults the super admin needs to triage. The exception:
    // when a CodedError carries an internalMessage, persist it under
    // a debug_events flag so admins can still trace deliberate-throw
    // patterns. (Only 5xx CodedErrors get persisted automatically.)
    if (error.statusCode >= 500) {
      void captureErrorEvent({
        statusCode: error.statusCode,
        error,
        metadata: error instanceof CodedError ? { internalMessage: error.internalMessage } : {},
      });
    }
    return NextResponse.json(body, { status: error.statusCode, headers });
  }

  if (error instanceof ZodError) {
    const body: Record<string, unknown> = {
      error: 'Validation failed',
      code: 'VALIDATION_ERROR',
      details: error.issues.map((e) => ({
        field: e.path.join('.'),
        message: e.message,
      })),
    };
    if (requestId) body.requestId = requestId;
    return NextResponse.json(body, { status: 400, headers });
  }

  // Unhandled - full details to pino + persist to error_events.
  logger.error({ err: error }, 'Unhandled error');
  void captureErrorEvent({ statusCode: 500, error });

  const body: Record<string, unknown> = { error: 'Internal server error', code: 'INTERNAL' };
  if (requestId) {
    body.requestId = requestId;
    body.message = `Something went wrong on our end. Quote error ID ${requestId} when reporting this.`;
  }
  return NextResponse.json(body, { status: 500, headers });
}
