import Honeybadger from '@honeybadger-io/js';
import { NextRequest, NextResponse } from 'next/server';
import * as nextServer from 'next/server';
import type { NextApiRequest, NextApiResponse } from 'next';
import {
  emitNodeRequestEvent,
  emitRequestEvent,
  insightsHttpEnabled,
  now,
  seedNodeRequestEventContext,
  seedRequestEventContext,
} from './insights-instrumentation';
import type { NodeRequestLike } from './insights-instrumentation';

type WaitUntil = (promise: Promise<unknown>) => void

/**
 * The `waitUntil` primitive the hosting platform injects per request. Next.js
 * resolves `after()` through this same accessor, and it is the only channel
 * available in Pages Router API routes, which are invoked as `(req, res)` with
 * no context argument to read it from.
 */
function requestContextWaitUntil(): WaitUntil | undefined {
  const context = (globalThis as Record<symbol, unknown>)[Symbol.for('@next/request-context')] as
    | { get?: () => { waitUntil?: WaitUntil } | undefined }
    | undefined
  const waitUntil = context?.get?.()?.waitUntil
  return typeof waitUntil === 'function' ? waitUntil : undefined
}

/**
 * Middleware receives a `NextFetchEvent` as its second argument. Duck-typed
 * rather than `instanceof` so the edge bundle needs no runtime import, and
 * bound because `waitUntil` is a class method that collects into the event.
 */
function eventWaitUntil(event: unknown): WaitUntil | undefined {
  const waitUntil = (event as { waitUntil?: WaitUntil } | undefined)?.waitUntil
  return typeof waitUntil === 'function' ? waitUntil.bind(event) : undefined
}

/**
 * Ensure Insights events are delivered before the serverless/edge runtime
 * freezes, without delaying the response where the runtime lets us avoid it.
 *
 * In order of preference: Next.js `after()` (stable in 15.1, App Router only —
 * it needs App Router request context, so Pages Router must not call it), then
 * a `waitUntil` from the middleware event or the platform request context,
 * then a blocking `flushAsync()` when the runtime offers neither. Blocking is
 * correct in that last case: no `waitUntil` means nothing is going to freeze
 * the invocation out from under us.
 *
 * Delivery failures are logged by the events worker and must not break the handler.
 */
function scheduleFlush(options: { useAfter?: boolean; waitUntil?: WaitUntil } = {}): Promise<void> | void {
  const flush = () => Honeybadger.flushAsync().catch(() => { /* logged by the events worker */ })

  if (options.useAfter) {
    const after = (nextServer as { after?: (cb: () => unknown) => void }).after
    if (typeof after === 'function') {
      // Exported but still refusable: `after()` throws outside a supported
      // context. Fall through to the remaining strategies rather than failing
      // the request.
      try {
        after(flush)
        return
      }
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      catch (error) {
        // try waitUntil / blocking flush below
      }
    }
  }

  const waitUntil = options.waitUntil ?? requestContextWaitUntil()
  if (waitUntil) {
    waitUntil(flush())
    return
  }

  return flush()
}

function configure(overrides?: Parameters<typeof Honeybadger.configure>[0]) {
  if (Honeybadger.config.apiKey?.length > 0) {
    return;
  }

  let projectRoot: string | undefined = undefined;
  try {
    // not available on edge runtime
    projectRoot = process.cwd();
  }
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  catch (error) {
    // do nothing
  }

  Honeybadger
    .configure({
      apiKey: process.env.NEXT_PUBLIC_HONEYBADGER_API_KEY,
      environment: process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.VERCEL_ENV || process.env.NODE_ENV,
      revision: process.env.NEXT_PUBLIC_HONEYBADGER_REVISION,
      projectRoot: 'webpack://_N_E/./',
      ...overrides,
    })
    .beforeNotify((notice) => {
      if (!projectRoot) {
        return
      }

      notice?.backtrace.forEach((line) => {
        if (line.file) {
          line.file = line.file.replace(`${projectRoot}/.next/server`, `${process.env.NEXT_PUBLIC_HONEYBADGER_ASSETS_URL}/..`)
        }
        return line
      })
    })
}

/**
 * Next.js uses thrown errors for control flow: `redirect()`, `notFound()`,
 * `forbidden()` and `unauthorized()` all throw an error carrying a `digest`
 * string (`NEXT_REDIRECT;...`, `NEXT_NOT_FOUND`, `NEXT_HTTP_ERROR_FALLBACK;...`).
 * These are not real failures — the framework catches them upstream to produce
 * the redirect/404/etc. — so we must let them propagate without reporting them,
 * otherwise every redirect shows up as an error in Honeybadger.
 *
 * We match on the `NEXT_` prefix rather than an exhaustive list so that any
 * present or future framework control-flow digest is covered. This is safe:
 * genuine errors that React tags with a `digest` use an opaque hash, and other
 * Next.js bailout signals (e.g. `BAILOUT_TO_CLIENT_SIDE_RENDERING`,
 * `DYNAMIC_SERVER_USAGE`) are not `NEXT_`-prefixed, so neither is skipped.
 */
function isNextControlFlowError(error: unknown): boolean {
  const digest = (error as { digest?: unknown } | null | undefined)?.digest
  return typeof digest === 'string' && digest.startsWith('NEXT_')
}

type AppRouterHandler = (req: NextRequest | Request, ...args: unknown[]) => Promise<NextResponse>
type PagesApiHandler = (req: NextApiRequest, res: NextApiResponse, ...args: unknown[]) => unknown

/**
 * Detects a Pages Router API invocation: `(req, res)` where `res` is a Node
 * `ServerResponse`. We branch on this structurally because — unlike an App
 * Router route handler — there is no returned `Response` to read the status
 * from; it lives on `res.statusCode`.
 */
function isPagesApiInvocation(args: unknown[]): args is [NodeRequestLike, NextApiResponse] {
  const req = args[0] as { headers?: unknown } | undefined
  const res = args[1] as { statusCode?: unknown; end?: unknown } | undefined
  return (
    !!req && typeof req.headers === 'object' && req.headers !== null &&
    !!res && typeof res.statusCode === 'number' && typeof res.end === 'function'
  )
}

/**
 * App Router route handlers and middleware: a web `Request`/`NextRequest` in, a
 * `Response`/`NextResponse` out. The status comes from the returned response.
 *
 * `waitUntil` is present for middleware (from its `NextFetchEvent`); route
 * handlers get `{ params }` as their second argument and rely on `after()`.
 */
async function handleAppRouterRequest(call: () => unknown, req: Request, canIsolate: boolean, waitUntil?: WaitUntil): Promise<unknown> {
  const ids = seedRequestEventContext(req.headers)
  if (canIsolate) {
    Honeybadger.setEventContext(ids)
  }
  const start = insightsHttpEnabled() ? now() : null
  try {
    const response = await call()
    if (start !== null) {
      emitRequestEvent(req, (response as Response | undefined)?.status, start, ids)
      await scheduleFlush({ useAfter: true, waitUntil })
    }
    return response
  } catch (error) {
    if (isNextControlFlowError(error)) {
      throw error
    }
    if (start !== null) {
      emitRequestEvent(req, 500, start, ids)
      await scheduleFlush({ useAfter: true, waitUntil })
    }
    await Honeybadger.notifyAsync(error as Error)
    throw error
  }
}

/**
 * Pages Router API routes: a Node `req`/`res` pair. The handler writes to `res`
 * and returns nothing meaningful, so the final status is read from
 * `res.statusCode` once it resolves.
 */
async function handlePagesApiRequest(call: () => unknown, req: NodeRequestLike, res: NextApiResponse, canIsolate: boolean): Promise<unknown> {
  const ids = seedNodeRequestEventContext(req.headers)
  if (canIsolate) {
    Honeybadger.setEventContext(ids)
  }
  const start = insightsHttpEnabled() ? now() : null
  try {
    const result = await call()
    if (start !== null) {
      emitNodeRequestEvent(req, res.statusCode, start, ids)
      // No after() here: Pages Router lacks the App Router request context it
      // needs. scheduleFlush falls through to the platform waitUntil instead.
      await scheduleFlush({ useAfter: false })
    }
    return result
  } catch (error) {
    if (isNextControlFlowError(error)) {
      throw error
    }
    if (start !== null) {
      emitNodeRequestEvent(req, 500, start, ids)
      await scheduleFlush({ useAfter: false })
    }
    await Honeybadger.notifyAsync(error as Error)
    throw error
  }
}

/**
 * Unrecognised invocation shape: still report errors, but emit no insights
 * event since we can't reliably read the request.
 */
async function handleUninstrumented(call: () => unknown): Promise<unknown> {
  try {
    return await call()
  } catch (error) {
    if (isNextControlFlowError(error)) {
      throw error
    }
    await Honeybadger.notifyAsync(error as Error)
    throw error
  }
}

/**
 * Wraps a handler function with Honeybadger error reporting. Works with App
 * Router route handlers, middleware, and Pages Router API routes.
 *
 * `request_id` / `correlation_id` are read from the `x-request-id` /
 * `request-id` and `x-correlation-id` / `x-amzn-trace-id` headers (generated
 * when absent). When `insights: { enabled: true, http: true }` is configured,
 * a `request.handled` event carrying the ids plus method, path, status and
 * duration is emitted per request.
 *
 * On the Node.js runtime each invocation additionally runs inside
 * `Honeybadger.run(...)`, so context is isolated per request and the ids are
 * seeded onto the event context — merged onto every event emitted during the
 * request, including programmatic `Honeybadger.event(...)` calls. On the edge
 * runtime (browser build, single global store) seeding the shared event
 * context would leak ids between concurrent requests, so programmatic events
 * there don't inherit them.
 *
 * The webpack config-file auto-injection (`honeybadger.*.config.js`) doesn't
 * reach API routes or edge middleware, so pass `config` to configure
 * Honeybadger explicitly there. It's ignored if Honeybadger is already
 * configured (e.g. by the auto-injected file).
 */
export function withHoneybadger(handler: AppRouterHandler, config?: Parameters<typeof Honeybadger.configure>[0]): AppRouterHandler
export function withHoneybadger(handler: PagesApiHandler, config?: Parameters<typeof Honeybadger.configure>[0]): PagesApiHandler
export function withHoneybadger(handler: AppRouterHandler | PagesApiHandler, config?: Parameters<typeof Honeybadger.configure>[0]) {
  configure(config);
  return new Proxy(handler, {
    apply: (target, thisArg, args) => {
      const canIsolate = typeof Honeybadger.run === 'function'
      const call = () => Reflect.apply(target, thisArg, args)

      const invoke = (): unknown => {
        // App Router / middleware first: a web Request as the first argument.
        if (typeof Request !== 'undefined' && args[0] instanceof Request) {
          return handleAppRouterRequest(call, args[0], canIsolate, eventWaitUntil(args[1]))
        }
        // Pages Router API route: a Node req/res pair.
        if (isPagesApiInvocation(args)) {
          return handlePagesApiRequest(call, args[0], args[1], canIsolate)
        }
        return handleUninstrumented(call)
      }

      return canIsolate ? Honeybadger.run(invoke) : invoke()
    },
  });
}
