/**
 * middleware/userErrors.js
 *
 * Standardises the user-facing error response shape and routes real
 * information to the operator alert bus instead.
 *
 * Every error that leaves the API to a non-admin caller looks like:
 *
 *   { error: "WE'RE_LOOKING_INTO_IT",  // stable code, not a real error class
 *     ref: "a1b2c3d4",               // short hex ref to the alert row
 *     message: "Something went wrong on our side. Reference a1b2c3d4 if you contact support." }
 *
 * Admins see the full detail via /admin/alerts?ref=a1b2c3d4.
 *
 * Usage:
 *   const err = new UserError('PLAN_LIMIT', 'You have used all your scans for this month.', { plan: 'free' });
 *   return res.status(402).json(err.publicView());
 *
 * Or via the helper:
 *   return sendUserError(res, 402, 'PLAN_LIMIT', 'You have used all your scans for this month.', { plan: 'free' });
 */

'use strict';

const alertBus = require('../services/alertBus');

/**
 * Stable, non-leaky user-facing error codes. NEVER add raw exception text
 * to these — keep them a fixed enum the UI can map to translations.
 */
const CODES = {
  // Generic
  INTERNAL:        { status: 500, label: 'INTERNAL',                publicMessage: 'Something went wrong on our side. Our team has been notified.' },
  NOT_FOUND:       { status: 404, label: 'NOT_FOUND',               publicMessage: "We couldn't find what you were looking for." },
  FORBIDDEN:       { status: 403, label: 'FORBIDDEN',               publicMessage: "You don't have access to this." },
  UNAUTHENTICATED: { status: 401, label: 'UNAUTHENTICATED',         publicMessage: 'Please sign in to continue.' },
  RATE_LIMITED:    { status: 429, label: 'RATE_LIMITED',            publicMessage: "You're going a bit fast — please wait a moment and try again." },
  // Domain-specific
  PLAN_LIMIT:       { status: 402, label: 'PLAN_LIMIT',              publicMessage: "You've reached your plan's limit. Upgrade to continue." },
  CSRF_MISMATCH:    { status: 403, label: 'CSRF_MISMATCH',           publicMessage: 'Your session expired. Refresh the page and try again.' },
  ACCOUNT_LOCKED:   { status: 423, label: 'ACCOUNT_LOCKED',          publicMessage: 'Too many attempts. Try again later.' },
  SUSPENDED:        { status: 403, label: 'SUSPENDED',               publicMessage: 'Your account is suspended. Contact support.' },
  INVALID_INPUT:    { status: 400, label: 'INVALID_INPUT',           publicMessage: "Some of the information you sent wasn't valid. Please review and try again." },
  DISPATCH_FAILED:  { status: 502, label: 'DISPATCH_FAILED',         publicMessage: "We couldn't complete that action. Our team has been notified." },
  TWO_FA_REQUIRED:  { status: 401, label: 'TWO_FA_REQUIRED',        publicMessage: 'Enter your authenticator code to continue.' },
  // Catch-all for unknown
  UNKNOWN:          { status: 500, label: 'WE_ARE_LOOKING_INTO_IT', publicMessage: "Something went wrong. Our team has been notified." },
};

function resolveCode(code) {
  return CODES[code] || CODES.UNKNOWN;
}

class UserError extends Error {
  constructor(code, opts = {}) {
    super(resolveCode(code).publicMessage);
    this.name = 'UserError';
    this.code = code;
    this.status = opts.status || resolveCode(code).status;
    this.publicMessage = opts.publicMessage || resolveCode(code).publicMessage;
    this.adminDetail = opts.adminDetail || null;  // goes to the alert
    this.alertSeverity = opts.alertSeverity || 'error';
    this.alertCategory = opts.alertCategory || 'other';
    this.alertService  = opts.alertService  || null;
    this.refUrl        = opts.refUrl        || null;
    this.dedupeKey     = opts.dedupeKey     || null;
    this.actorId       = opts.actorId       || null;
    this.skipAlert     = !!opts.skipAlert;
  }
  publicView(ref) {
    return {
      error:   this.code,
      ref:     ref || null,
      message: this.publicMessage,
    };
  }
}

/**
 * Send a user-facing error and (unless skipAlert) raise an internal alert
 * carrying the real detail.
 */
function sendUserError(res, code, opts = {}) {
  const def = resolveCode(code);
  const status = opts.status || def.status;
  const message = opts.publicMessage || def.publicMessage;
  let ref = null;
  if (!opts.skipAlert) {
    const r = alertBus.raise({
      severity:   opts.alertSeverity || (status >= 500 ? 'error' : 'warn'),
      category:   opts.alertCategory || 'other',
      service:    opts.alertService  || null,
      title:      opts.alertTitle    || `${code}: ${message}`,
      detail:     opts.adminDetail   || null,
      ref_url:    opts.refUrl        || null,
      actor_id:   opts.actorId       || null,
      dedupe_key: opts.dedupeKey     || null,
    });
    ref = r.alert ? r.alert.ref : null;
  }
  return res.status(status).json({
    error: code,
    ref:   ref,
    message: message,
  });
}

/**
 * Wrap an async route handler so uncaught errors are converted to a
 * non-leaky generic response. Real detail goes to the alert bus.
 *
 *   router.post('/x', requireLogin, asyncRoute(async (req, res) => {
 *     ...
 *     throw new UserError('PLAN_LIMIT');
 *   }));
 */
function asyncRoute(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch((err) => {
      if (err instanceof UserError) {
        return sendUserError(res, err.code, err);
      }
      // Unknown error — log + alert + generic 500
      console.error('[asyncRoute] uncaught:', err);
      return sendUserError(res, 'INTERNAL', {
        adminDetail: { message: err.message, stack: err.stack && err.stack.split('\n').slice(0, 5).join('\n') },
        alertTitle:  `Unhandled route error on ${req.method} ${req.path}`,
        alertCategory:'other',
        refUrl:      `${req.method} ${req.path}`,
        dedupeKey:   `route_error:${req.method}:${req.path}`,
      });
    });
  };
}

/**
 * The existing routes have ad-hoc error returns. Provide a small helper that
 * the existing code can swap to without rewriting every call site.
 */
function maybePublicView(errOrCode) {
  if (errOrCode && typeof errOrCode === 'object' && errOrCode.code && CODES[errOrCode.code]) {
    return {
      error:   errOrCode.code,
      message: errOrCode.message || CODES[errOrCode.code].publicMessage,
    };
  }
  return null;
}

module.exports = { UserError, sendUserError, asyncRoute, CODES, resolveCode, maybePublicView };
