import { withAuth } from "next-auth/middleware";
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import { rateLimiter } from "@/lib/rate-limiter";
import { generateCspNonce, attachCspHeader } from "@/lib/security/csp";

// PR-18 (May 2026): per-request CSP nonce. Generated at the top of
// each middleware invocation, propagated to the route handler via the
// `x-nonce` request header (so layout.tsx can pick it up via
// next/headers), and emitted in the `Content-Security-Policy` response
// header so framework scripts auto-inherit it. See src/lib/security/csp.ts
// for the directive structure + dev/prod trade-offs.

// audit3-followup (2026-05-30): the legacy executive_session handoff gate
// (handleExecutiveAuth) was removed with the executive dashboard rebuild. The
// redesigned /executive is login-less via the executive_display token system
// (src/lib/executive-display-auth.ts), gated client-side by ExecutiveAuthWrapper;
// /api/reports/executive + /api/executive authorize themselves. The API paths
// stay matcher-excluded so middleware never enforces NextAuth on them.
//
// wall-blank fix (2026-05-30): the /executive PAGE routes are NO LONGER
// matcher-excluded — they need the per-request CSP nonce (PR-18) or their
// framework <script> tags ship without a nonce and a 'strict-dynamic' CSP
// blocks every chunk (blank dark screen). They run the auth-bypassed
// public-page branch below (like /status), so the login-less design holds.

// P0-6.5: paths a non-enrolled user is allowed to visit so they can
// actually complete the 2FA enrollment flow. Without these, the
// enforcement redirect would loop the user back to themselves.
const ENROLLMENT_ALLOWLIST = [
    '/enroll-2fa',
    '/settings',          // 2FA setup lives in the settings page
    '/api/user/2fa',      // GET secret/QR, POST enable, DELETE disable
    '/api/auth',          // NextAuth endpoints (signout, etc)
    '/api/notifications', // small read endpoints used by the shell
];

function isEnrollmentAllowlisted(pathname: string): boolean {
    return ENROLLMENT_ALLOWLIST.some((p) => pathname === p || pathname.startsWith(p + '/'));
}

// OAuth interstitial 2FA (auditor 2026-06-01 follow-up). Paths a
// requires2faOauth user is allowed to reach so they can submit the
// TOTP and clear the flag. Without this, the redirect loops on itself.
const OAUTH_2FA_ALLOWLIST = [
    '/2fa-challenge-oauth',
    '/api/auth',                  // NextAuth signout/session/etc.
    '/api/auth/2fa-verify-oauth', // The TOTP submit endpoint.
];

function isOauth2faAllowlisted(pathname: string): boolean {
    return OAUTH_2FA_ALLOWLIST.some((p) => pathname === p || pathname.startsWith(p + '/'));
}

const authMiddleware = withAuth(
    async function middleware(req) {
        // Rate Limit API Routes (100 reqs/min)
        if (req.nextUrl.pathname.startsWith('/api')) {
            const limitResponse = rateLimiter(req);
            if (limitResponse) return limitResponse;
        }

        // Retrieve token using getToken helper which is robust
        const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });

        const isPending = token?.role === 'PENDING';
        const isApprovalPage = req.nextUrl.pathname === '/pending-approval';

        if (isPending && !isApprovalPage) {
            return NextResponse.redirect(new URL('/pending-approval', req.url));
        }

        // P0-6.5: must-enrol gate. When AUTH_2FA_ENFORCED=true at the
        // server, any logged-in user without totpEnabled is forced to
        // /enroll-2fa until they complete setup. ENROLLMENT_ALLOWLIST
        // lets them reach the actual enrolment UI without hitting the
        // redirect loop.
        //
        // Edge runtime can't import @/lib/env (Zod throws on partial
        // process.env), so we read AUTH_2FA_ENFORCED via process.env
        // directly. Same coerce logic as the Zod preprocess.
        const enforced = process.env.AUTH_2FA_ENFORCED === 'true'
            || process.env.AUTH_2FA_ENFORCED === '1';
        if (enforced && token && token.totpEnabled === false) {
            if (!isEnrollmentAllowlisted(req.nextUrl.pathname)) {
                // PR-18.1: API routes get a JSON 403, not a 307 redirect.
                // EventSource / fetch().then(r => r.json()) callers
                // explode on a redirect to /enroll-2fa (the latter
                // tries to parse the redirect target's HTML body as
                // JSON, producing the "Unexpected token '<'" error in
                // the browser console).
                if (req.nextUrl.pathname.startsWith('/api')) {
                    return NextResponse.json(
                        { error: '2fa-enrollment-required', message: 'Complete 2FA setup at /enroll-2fa.' },
                        { status: 403 },
                    );
                }
                const url = new URL('/enroll-2fa', req.url);
                return NextResponse.redirect(url);
            }
        }

        // OAuth interstitial 2FA (auditor 2026-06-01 follow-up). If the
        // JWT was issued via Google/Entra for a user with totpEnabled=true
        // (jwt callback set requires2faOauth=true), block dashboard access
        // until the user submits a TOTP code at /2fa-challenge-oauth.
        // Same JSON-vs-redirect split as the enroll gate above.
        if (enforced && token && token.requires2faOauth === true) {
            if (!isOauth2faAllowlisted(req.nextUrl.pathname)) {
                if (req.nextUrl.pathname.startsWith('/api')) {
                    return NextResponse.json(
                        {
                            error: 'oauth-2fa-challenge-required',
                            message: 'Complete the OAuth 2FA challenge at /2fa-challenge-oauth.',
                        },
                        { status: 403 },
                    );
                }
                return NextResponse.redirect(new URL('/2fa-challenge-oauth', req.url));
            }
        }
    },
    {
        callbacks: {
            authorized: ({ token }) => !!token,
        },
    }
);

export default async function proxy(req: NextRequest) {
    const nonce = generateCspNonce();
    const isDev = process.env.NODE_ENV !== 'production';
    const { pathname } = req.nextUrl;

    // PR-18: public + auth-bypassed paths still need a CSP. The
    // matcher now includes them so middleware fires; we short-circuit
    // the auth pipeline and attach the nonce'd CSP directly.
    //
    // wall-blank fix (2026-05-30): the login-less /executive/* pages are
    // auth-bypassed exactly like /status — they self-gate client-side via
    // ExecutiveAuthWrapper, and /api/executive authorizes itself. They were
    // previously matcher-EXCLUDED entirely, which meant the proxy never set
    // the `x-nonce` request header, so Next rendered the framework <script>
    // tags WITHOUT a nonce. Under a 'strict-dynamic' CSP that revokes the
    // 'self' allowlist, every chunk was blocked → the .noc dark shell paints
    // but React never hydrates → blank screen (no token form). Routing the
    // /executive PAGES through the public-page branch gives them the nonce +
    // a matching CSP, while /api/executive + /api/reports/executive stay
    // matcher-excluded (JSON, self-authorizing, need no nonce).
    const isExecutivePage = pathname === '/executive' || pathname.startsWith('/executive/');
    const isPublicPage = pathname === '/login'
        || pathname === '/pending-approval'
        || pathname.startsWith('/status')
        || isExecutivePage;
    if (isPublicPage) {
        // Hotfix (login URL credential leak): if a GET /login ever
        // arrives carrying `username` or `password` in the query string
        // (which would happen if the client-side form fell back to a
        // native HTML submit), 302 to a clean URL BEFORE rendering the
        // page. This stops:
        //   - the access log from recording the password,
        //   - the URL from sitting in browser history,
        //   - the Referer header from leaking it to third-party assets
        //     loaded by the login page.
        // The clean redirect uses Referrer-Policy: no-referrer so the
        // redirect itself doesn't leak the dirty URL.
        if (pathname === '/login') {
            const sp = req.nextUrl.searchParams;
            if (sp.has('username') || sp.has('password')) {
                const cleanUrl = new URL(req.url);
                cleanUrl.searchParams.delete('username');
                cleanUrl.searchParams.delete('password');
                const redirect = NextResponse.redirect(cleanUrl, 303);
                redirect.headers.set('Referrer-Policy', 'no-referrer');
                redirect.headers.set('Cache-Control', 'no-store');
                return attachCspHeader(redirect, nonce, isDev);
            }
        }
        const requestHeaders = new Headers(req.headers);
        requestHeaders.set('x-nonce', nonce);
        const response = NextResponse.next({ request: { headers: requestHeaders } });
        // Tighten Referrer-Policy specifically for /login so even if
        // anything ever ends up in the URL, browser-internal navigation
        // (e.g. clicking the Google sign-in icon image link) cannot
        // leak the URL via Referer. Overrides the global
        // strict-origin-when-cross-origin (which still leaks same-origin).
        if (pathname === '/login') {
            response.headers.set('Referrer-Policy', 'no-referrer');
        }
        return attachCspHeader(response, nonce, isDev);
    }

    // Strict brute-force protection on login credentials endpoint
    // Limits to 5 attempts per 1 hour per IP — separate from the general 100/min API limit
    if (pathname === '/api/auth/callback/credentials' && req.method === 'POST') {
        const loginLimit = rateLimiter(req, { limit: 5, windowMs: 60 * 60 * 1000 });
        if (loginLimit) return attachCspHeader(loginLimit, nonce, isDev);
    }

    // Inject `x-nonce` into the request so layout.tsx can read it via
    // next/headers and Next.js can auto-attach the nonce to framework
    // <script> tags. Without this hop the response CSP would name a
    // nonce that no script tag declares, and the browser would block
    // every script.
    const requestHeaders = new Headers(req.headers);
    requestHeaders.set('x-nonce', nonce);

    // Delegate to NextAuth for everything else
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const authResponse = await (authMiddleware as unknown as any)(req);

    // withAuth may return a NextResponse-like object (redirect / json)
    // or undefined (continue). When undefined, build NextResponse.next()
    // that carries the x-nonce request header. We duck-type on `.headers`
    // because the runtime type can be either `NextResponse` or a plain
    // web `Response` depending on whether next-auth intercepted.
    const response: NextResponse = (authResponse && typeof authResponse.headers?.set === 'function')
        ? authResponse as NextResponse
        : NextResponse.next({ request: { headers: requestHeaders } });

    return attachCspHeader(response, nonce, isDev);
}

export const config = {
    matcher: [
        /*
         * Match all request paths except for the ones starting with:
         * - login (login page)
         * - api/auth (auth endpoints — NextAuth handles its own flow)
         * - api/webhooks (webhook endpoints)
         * - api/cron (cron jobs — secured by CRON_SECRET env var)
         * - api/reports/executive, api/executive (login-less display token
         *   APIs — self-authorizing JSON; excluded so withAuth never runs.
         *   NOTE: the /executive PAGE routes are intentionally NOT excluded
         *   anymore — they need the CSP nonce, see the wall-blank note above.)
         * - status (public status page)
         * - pending-approval (allow access to approval page to loop)
         * - _next/static (static files)
         * - _next/image (image optimization files)
         * - favicon.ico (favicon file)
         *
         * NOTE: api/sse/dashboard and api/dashboard/stats are intentionally
         * NOT excluded — they perform their own auth checks internally.
         *
         * PR-B.4: api/health excluded so orchestrators (Docker/k8s/ECS)
         * can probe liveness without auth. The shallow /api/health is
         * intentionally unauthenticated (just "is the process up?");
         * /api/health/deep enforces its own Authorization: Bearer
         * $CRON_SECRET check at the route handler.
         *
         * PR-18: `login`, `status`, and `pending-approval` are NO LONGER
         * matcher-excluded. Middleware still bypasses auth for them
         * (`isPublicPage` early return) but attaches the CSP nonce so
         * the login form + public status page get the same XSS-safe
         * Content-Security-Policy as authenticated pages.
         *
         * PR-19: `api/status` is excluded — the public
         * subscribe/confirm/unsubscribe endpoints are anonymous by
         * design. Rate limiting still applies because the matcher
         * exclusion only skips withAuth, not the broader rate-limit
         * wrapper at the route layer.
         *
         * audit3-followup (2026-05-29): `api/csp-report` is excluded
         * so browsers can POST CSP violation reports without a session
         * cookie. The endpoint is rate-limited at the route layer and
         * always returns 204 regardless of body validity.
         *
         * Static-asset extensions (png/jpg/svg/etc.) are excluded so
         * files dropped into /public/ load without an auth round-trip
         * — otherwise /login.png on the login page gets a 307 to the
         * signin route and renders as a broken image. /public/ is
         * world-readable by definition; nothing sensitive should live
         * there.
         */
        "/((?!api/auth|api/webhooks|api/cron|api/health|api/status|api/csp-report|api/reports/executive|api/executive|_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|otf)$).*)",
    ],
};
