import {
  ClerkMiddlewareOptions,
  clerkMiddleware,
  createRouteMatcher,
} from '@clerk/nextjs/server';
import { geolocation } from '@vercel/functions';
import buildGetJwks from 'get-jwks';
import { NextResponse } from 'next/server';

import { isDev, isStaging } from './utils/environment';

const isAdminRoute = createRouteMatcher(['/b-side', '/b-side/(.*)']);

const getJwks = buildGetJwks({
  jwksPath: '/api/clerk/v1/jwks',
  providerDiscovery: false,
});

// Routes that can be accessed by signed in + out users
const isPublicRoute = createRouteMatcher([
  '/',
  '/home',
  '/home/advanced',
  '/discover',
  '/login',
  '/sign-in',
  '/sign-up',
  '/auth/verify',
  '/auth/error',
  '/embed/(.*)',
  '/invite/(.*)',
  '/persona/(.*)',
  '/playlist/(.*)',
  '/profile/:username', // match profile/username but still auth follower/following pages
  '/song/(.*)',
  '/hook/(.*)',
  '/profile/:username/hook/(.*)',
  '/s/(.*)',
  '/oauth-redirect(.*)',
  '/link-account',
  '/link-account/(.*)',
  '/students(.*)',
  '/christmas(.*)',
  '/2025(.*)',
  '/songs-of-love(.*)',
  '/@(.*)',
  '/.well-known/apple-app-site-association',
  '/pricing',
  '/home/c/(.*)',
  '/coffey',
  '/imoliver',
  '/blackparty',
  '/live-radio',
  '/live-radio/(.*)',
  '/api/billing/webhook',
  '/api/billing/webhook/',
  '/studio-waitlist',
  '/studio-waitlist/(.*)',
  '/studio-welcome',
  '/studio-welcome/(.*)',
  '/collab/(.*)',
  '/contest/(.*)',
  '/hub',
  '/hub/(.*)',
  '/hub-sitemap.xml',
]);

const ignoredRoutes = createRouteMatcher(['/v1/api/(.*)', '/api/(.*)']);

const CLERK_KEYS_WHITELIST = [
  // auth.suno.fm
  'pk_live_YXV0aC5zdW5vLmZtJA',
  'pk_test_YXV0aC5zdW5vLmZtJA',
  // auth.suno.com
  'pk_live_YXV0aC5zdW5vLmNvbSQ',
  'pk_test_YXV0aC5zdW5vLmNvbSQ',
];

const ISSUERS_WHITELIST = [
  'https://suno.com',
  'https://suno.fm',
  'https://auth.suno.com',
  'https://auth.suno.fm',
];

// Specifically exclude billing webhook from any auth checks
const isBillingWebhookRoute = createRouteMatcher([
  '/api/billing/webhook',
  '/api/billing/webhook/',
]);

const HAS_LOGGED_IN_BEFORE = 'has_logged_in_before';

export default clerkMiddleware(
  async (auth, req) => {
    const requestHeaders = new Headers(req.headers);
    const { country } = geolocation(req);

    if (country) {
      requestHeaders.set('country', country);
    }

    const session = await auth();

    // signed-in users redirect from /home
    if (req.nextUrl.pathname === '/home' && session.userId) {
      const url = new URL('/', req.url);
      const searchParams = req.nextUrl.searchParams;
      searchParams.forEach((value, key) => {
        url.searchParams.set(key, value);
      });
      return NextResponse.redirect(url);
    }

    // Add specific check for root path and logged out users
    if (req.nextUrl.pathname === '/' && !session.userId) {
      const hasLoggedInBefore = req.cookies.get(HAS_LOGGED_IN_BEFORE);
      if (!hasLoggedInBefore) {
        const homeUrl = new URL('/home', req.url);
        // Preserve URL parameters from the original request
        const searchParams = req.nextUrl.searchParams;
        searchParams.forEach((value, key) => {
          homeUrl.searchParams.set(key, value);
        });
        return NextResponse.redirect(homeUrl);
      }
    }

    // if authed and going to pricing, redirect to account
    if (req.nextUrl.pathname === '/pricing' && session.userId) {
      const url = new URL('/account', req.url);
      const searchParams = req.nextUrl.searchParams;
      searchParams.forEach((value, key) => {
        url.searchParams.set(key, value);
      });
      return NextResponse.redirect(url);
    }

    // if not authed and going to account, redirect to pricing
    if (req.nextUrl.pathname === '/account' && !session.userId) {
      const url = new URL('/pricing', req.url);
      const searchParams = req.nextUrl.searchParams;
      searchParams.forEach((value, key) => {
        url.searchParams.set(key, value);
      });
      return NextResponse.redirect(url);
    }

    // temp hide pricing page, redirect to home
    // note also update sitemap
    // if (req.nextUrl.pathname === '/pricing') {
    //   const url = new URL('/', req.url);
    //   return NextResponse.redirect(url);
    // }

    if (req.nextUrl.pathname === '/studio-waitlist') {
      const url = new URL('/studio-welcome', req.url);
      return NextResponse.redirect(url);
    }

    if (isAdminRoute(req) && !isStaging && !isDev) {
      const email = session.sessionClaims?.[
        'https://suno.ai/claims/email'
      ] as string;

      // :i-pretend-i-do-not-see-it:
      if (
        !email ||
        !(email.endsWith('@suno.ai') || email.endsWith('@suno.com'))
      ) {
        return NextResponse.rewrite(new URL('/not-found', req.nextUrl));
      }
    }

    // Ensure billing webhook routes are completely bypassed (check before protected routes)
    if (isBillingWebhookRoute(req)) {
      return NextResponse.next({
        request: {
          headers: requestHeaders,
        },
      });
    }

    // Protected routes
    if (!isPublicRoute(req) && !ignoredRoutes(req)) {
      await auth.protect();
    }

    return NextResponse.next({
      request: {
        headers: requestHeaders,
      },
    });
  },
  async (req) => {
    let token: string | undefined;

    let logMode = '';

    // Extract JWT from the request (from Authorization header or cookie)
    const authHeader = req.headers.get('authorization');
    if (authHeader && authHeader.startsWith('Bearer ')) {
      token = authHeader.slice(7);
      logMode = 'auth-header';
    } else {
      // Try to get from __session cookie
      const sessionCookie = req.cookies.get('__session');
      if (sessionCookie && sessionCookie.value) {
        token = sessionCookie.value;
        logMode = 'session-cookie';
      }
    }

    // Extract handshake token from request parameters
    const handshakeToken = req.nextUrl.searchParams.get('__clerk_handshake');
    if (handshakeToken) {
      token = handshakeToken;
      logMode = 'handshake';
    }

    let iss: string | undefined;
    let kid: string | undefined;
    let alg: string | undefined;

    if (token) {
      // JWT format: header.payload.signature
      const parts = token.split('.');
      if (parts.length === 3) {
        try {
          // Decode header and payload
          const header = JSON.parse(
            Buffer.from(parts[0], 'base64url').toString('utf8')
          );
          const payload = JSON.parse(
            Buffer.from(parts[1], 'base64url').toString('utf8')
          );
          iss = payload.iss;
          kid = header.kid;
          alg = header.alg;
        } catch (e) {
          // ignore parse errors
        }
      }
    }

    let jwtKey: string | undefined;
    let publishableKey: string | undefined;

    // If the token was issued by Suno, validate using our own JWKS
    if (ISSUERS_WHITELIST.includes(iss || '')) {
      try {
        const publicKey = await getJwks.getPublicKey({
          domain: process.env.NEXT_PUBLIC_API_BASE || iss,
          kid: kid || 'suno-api-rs256-key-1',
          alg: alg || 'RS256',
        });
        jwtKey = publicKey;
      } catch (error) {
        console.error('Failed to fetch JWKS public key:', error);
      }
    }
    // if auth_suno cookie is set, set clerkPublishableKey to its value
    if (req.cookies) {
      const authSunoCookie = req.cookies.get('suno_auth');
      if (authSunoCookie) {
        let key =
          typeof authSunoCookie === 'string'
            ? authSunoCookie
            : authSunoCookie.value;
        if (CLERK_KEYS_WHITELIST.includes(key)) {
          publishableKey = key;
        }
      }
    }

    if (logMode !== '') {
      console.log(
        'logMode',
        logMode,
        'iss',
        iss,
        'kid',
        kid,
        'alg',
        alg,
        'jwtKey',
        jwtKey,
        'publishableKey',
        publishableKey
      );
    }
    return { jwtKey, publishableKey } as ClerkMiddlewareOptions;
  }
);

export const config = {
  matcher: [
    '/((?!.+\\.[\\w]+$|_next|v1|monitoring|hub).*)',
    '/',
    '/(api|trpc)(.*)',
    // registration flow used to allow handles with dots.
    // specifically match all profile (@) pages, even those with dots like @profile.name
    '/@(.*)',
  ],
};
