/**
 * Error reporting hook utilities for @beignet/core/server
 */

import type { HttpContractConfig } from "../../contracts/index.js";
import type {
  ErrorReporterPort,
  ErrorReportOptions,
} from "../../error-reporting/index.js";
import {
  DEFAULT_ERROR_REPORTING_TIMEOUT_MS,
  tryReportException,
} from "../../error-reporting/index.js";
import {
  errorReportingObserverTimeout,
  runErrorReportingOperation,
} from "../../error-reporting/internal.js";
import { isAppError, SchemaValidationError } from "../../errors/index.js";
import {
  IdempotencyConflictError,
  IdempotencyInProgressError,
} from "../../idempotency/index.js";
import {
  AuthUnauthorizedError,
  EntitlementRequiredError,
  GateAuthorizationError,
  TenantRequiredError,
} from "../../ports/index.js";
import type {
  HttpRequestLike,
  ServerCaughtErrorHook,
  ServerHook,
} from "../types.js";

type MaybePromise<T> = T | Promise<T>;

/**
 * Minimal context shape used by `createErrorReportingHooks(...)`.
 */
export type ErrorReportingContext = {
  ports?: {
    errorReporter?: ErrorReporterPort;
  };
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
  actor?: {
    type?: string;
    id?: string;
  };
  tenant?: {
    id?: string;
  };
};

/**
 * Arguments passed to error reporting hook callbacks.
 */
export type ErrorReportingHookArgs<Ctx> = Parameters<
  ServerCaughtErrorHook<Ctx>
>[0];

/**
 * Error reporter resolver accepted by `createErrorReportingHooks(...)`.
 */
export type ErrorReporterResolver<Ctx> = (
  args: ErrorReportingHookArgs<Ctx>,
) => MaybePromise<ErrorReporterPort | undefined>;

/**
 * Options accepted by `createErrorReportingHooks(...)`.
 */
export interface ErrorReportingHooksOptions<Ctx> {
  /**
   * Reporter to use instead of `ctx.ports.errorReporter`.
   */
  reporter?: ErrorReporterPort | ErrorReporterResolver<Ctx>;
  /**
   * Decide whether a caught error should be captured. The default reports
   * unexpected failures and skips expected catalog/auth/validation outcomes.
   */
  shouldReport?: (args: ErrorReportingHookArgs<Ctx>) => MaybePromise<boolean>;
  /**
   * Additional capture options. These are merged over Beignet's request,
   * actor, tenant, contract, and trace metadata.
   */
  reportOptions?:
    | ErrorReportOptions
    | ((args: ErrorReportingHookArgs<Ctx>) => MaybePromise<ErrorReportOptions>);
  /**
   * Maximum time allowed for reporting preparation, capture, and failure
   * observation. Each phase is bounded independently. Set to `false` only for
   * intentionally unbounded reporters.
   *
   * @default 1000
   */
  timeoutMs?: number | false;
  /**
   * Observer for reporter failures. Reporter failures are otherwise ignored so
   * error reporting cannot change the HTTP response.
   */
  onReporterError?: (args: {
    error: unknown;
    reportingError: unknown;
    ctx?: Ctx;
    req: HttpRequestLike;
    contract: HttpContractConfig;
  }) => MaybePromise<void>;
}

/**
 * Create server hooks that capture unexpected HTTP request failures through
 * `ctx.ports.errorReporter`.
 *
 * The hook observes `onCaughtError`, never maps responses, and ignores capture
 * failures by default so reporting cannot affect request handling.
 */
export function createErrorReportingHooks<
  Ctx extends ErrorReportingContext = ErrorReportingContext,
>(config: ErrorReportingHooksOptions<Ctx> = {}): ServerHook<Ctx> {
  return {
    name: "error-reporting",
    async onCaughtError(args) {
      const timeoutMs = config.timeoutMs ?? DEFAULT_ERROR_REPORTING_TIMEOUT_MS;
      let prepared:
        | { reporter: ErrorReporterPort; reportOptions: ErrorReportOptions }
        | undefined;

      try {
        prepared = await runErrorReportingOperation(async () => {
          const shouldReport = config.shouldReport
            ? await config.shouldReport(args)
            : shouldReportServerError(args.err);
          if (!shouldReport) return undefined;

          const reporter = await resolveReporter(config.reporter, args);
          if (!reporter) return undefined;

          const baseOptions = createDefaultReportOptions(args);
          const customOptions =
            typeof config.reportOptions === "function"
              ? await config.reportOptions(args)
              : config.reportOptions;

          return {
            reporter,
            reportOptions: mergeReportOptions(baseOptions, customOptions),
          };
        }, timeoutMs);
      } catch (reportingError) {
        await notifyReporterError(config, args, reportingError, timeoutMs);
        return;
      }

      if (!prepared) return;

      await tryReportException({
        reporter: prepared.reporter,
        error: args.err,
        reportOptions: prepared.reportOptions,
        timeoutMs,
        onReporterError: ({ reportingError }) =>
          config.onReporterError?.({
            error: args.err,
            reportingError,
            ctx: args.ctx,
            req: args.req,
            contract: args.contract,
          }),
      });
    },
  };
}

async function notifyReporterError<Ctx>(
  config: ErrorReportingHooksOptions<Ctx>,
  args: ErrorReportingHookArgs<Ctx>,
  reportingError: unknown,
  timeoutMs: number | false,
): Promise<void> {
  if (!config.onReporterError) return;

  try {
    await runErrorReportingOperation(
      () =>
        config.onReporterError?.({
          error: args.err,
          reportingError,
          ctx: args.ctx,
          req: args.req,
          contract: args.contract,
        }),
      errorReportingObserverTimeout(timeoutMs),
    );
  } catch {
    // Reporting observers must not affect request handling.
  }
}

/**
 * Default filter for HTTP error reporting.
 */
export function shouldReportServerError(error: unknown): boolean {
  if (isAppError(error)) return false;
  if (error instanceof AuthUnauthorizedError) return false;
  if (error instanceof TenantRequiredError) return false;
  if (error instanceof IdempotencyConflictError) return false;
  if (error instanceof IdempotencyInProgressError) return false;
  if (error instanceof GateAuthorizationError) return false;
  if (error instanceof EntitlementRequiredError) return false;
  if (error instanceof SchemaValidationError) return false;
  return true;
}

async function resolveReporter<Ctx extends ErrorReportingContext>(
  reporter: ErrorReportingHooksOptions<Ctx>["reporter"],
  args: ErrorReportingHookArgs<Ctx>,
): Promise<ErrorReporterPort | undefined> {
  if (typeof reporter === "function") {
    return reporter(args);
  }

  if (reporter) {
    return reporter;
  }

  return args.ctx?.ports?.errorReporter;
}

function createDefaultReportOptions<Ctx extends ErrorReportingContext>(
  args: ErrorReportingHookArgs<Ctx>,
): ErrorReportOptions {
  const ctx = args.ctx;
  const url = safeUrl(args.req.url);
  const actor = ctx?.actor;
  const tenant = ctx?.tenant;

  return {
    level: "error",
    mechanism: "beignet.server",
    handled: false,
    requestId: ctx?.requestId,
    traceId: ctx?.traceId,
    spanId: ctx?.spanId,
    parentSpanId: ctx?.parentSpanId,
    traceparent: ctx?.traceparent,
    user: actor?.type === "user" && actor.id ? { id: actor.id } : undefined,
    tags: {
      "beignet.kind": "http",
      "http.method": args.req.method,
      "http.route": args.contract.path,
      "http.contract": args.contract.name,
    },
    contexts: {
      request: {
        method: args.req.method,
        path: url?.pathname ?? args.contract.path,
        route: args.contract.path,
        contract: args.contract.name,
      },
      actor:
        actor?.type || actor?.id
          ? {
              type: actor.type,
              id: actor.id,
            }
          : undefined,
      tenant: tenant?.id ? { id: tenant.id } : undefined,
    },
  };
}

function mergeReportOptions(
  base: ErrorReportOptions,
  custom: ErrorReportOptions | undefined,
): ErrorReportOptions {
  if (!custom) return base;

  return {
    ...base,
    ...custom,
    tags: {
      ...base.tags,
      ...custom.tags,
    },
    contexts: {
      ...base.contexts,
      ...custom.contexts,
    },
    extra: {
      ...base.extra,
      ...custom.extra,
    },
  };
}

function safeUrl(value: string): URL | undefined {
  try {
    return new URL(value);
  } catch {
    return undefined;
  }
}
