import { NextResponse } from 'next/server';
import { createServerClient } from '@supabase/ssr';

const PUBLIC_PREFIXES = [
  '/login',
  '/share',
  '/api/auth',
  '/api/share',
  '/_next',
];

// Loopback tick routes are dispatched by the in-process cron/heartbeat daemons
// (lib/db.js) over localhost. They self-gate on the X-Inproc-Token header (or a
// session) inside their own route handlers — see lib/inproc-token.js. The
// middleware must NOT redirect them to /login in supabase auth mode, or the
// daemon's fetch follows the redirect, receives the login HTML, and fails JSON
// parsing ("Unexpected token '<'"). Bypassing here defers to the route's own
// token check, which is the authoritative code-level gate.
const SELF_GATED_PATHS = [
  '/api/cron/tick',
  '/api/heartbeat/worker',
];

const PUBLIC_FILES = [
  '/favicon.ico',
  '/madarx-mark.svg',
  '/madarx-logo.svg',
  '/robots.txt',
  '/sitemap.xml',
];

// Loopback-only tick routes dispatched by the in-process daemon (lib/db.js).
// They carry an X-Inproc-Token the daemon shares with the route handler in the
// same Node process. Middleware runs in a separate runtime instance and cannot
// compare the token value, so it only checks header PRESENCE and lets the
// request through to the route — which performs the real constant-time
// verifyInprocToken() check and falls back to requireSession() on mismatch.
// A forged header therefore gains nothing: it reaches the route's real gate.
const INPROC_TICK_PATHS = ['/api/cron/tick', '/api/heartbeat/worker'];

function isInprocTick(request) {
  return (
    INPROC_TICK_PATHS.includes(request.nextUrl.pathname) &&
    !!request.headers.get('x-inproc-token')
  );
}

function isPublicPath(pathname) {
  if (SELF_GATED_PATHS.includes(pathname)) return true;
  if (PUBLIC_FILES.includes(pathname)) return true;
  if (/\.(svg|png|jpg|jpeg|gif|webp|ico|css|js|map|txt|woff2?)$/i.test(pathname)) return true;
  return PUBLIC_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
}

function safeNext(pathname, search) {
  const next = `${pathname}${search || ''}`;
  return next.startsWith('/login') ? '/' : next;
}

// Runs in the Edge runtime — cannot read node:sqlite. Resolves the UI-driven
// auth config via the public /api/auth/config route, cached briefly in module
// scope. An explicit MADARX_AUTH_MODE env value is the fast-path backstop.
let _cfgCache = { at: 0, data: null };

async function resolveAuthConfig(request) {
  const envRaw = (process.env.MADARX_AUTH_MODE || '').trim().toLowerCase();
  if (envRaw === 'local' || envRaw === 'supabase') {
    return {
      authMode: envRaw,
      supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL || null,
      supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || null,
    };
  }
  const now = Date.now();
  if (_cfgCache.data && now - _cfgCache.at < 10_000) return _cfgCache.data;
  try {
    const resp = await fetch(new URL('/api/auth/config', request.nextUrl.origin), {
      headers: { accept: 'application/json' },
    });
    if (!resp.ok) throw new Error(`config HTTP ${resp.status}`);
    const data = await resp.json();
    _cfgCache = { at: now, data };
    return data;
  } catch {
    // Fail CLOSED, never open. Prefer the last-known-good config (even if stale)
    // over dropping the gate; if we have never resolved one, gate everything
    // (treat as supabase with no creds) so an outage can't expose the app.
    // /login is public, so this cannot hard-loop.
    if (_cfgCache.data) return _cfgCache.data;
    return { authMode: 'supabase', supabaseUrl: null, supabaseAnonKey: null, _unavailable: true };
  }
}

export async function middleware(request) {
  const { pathname, search } = request.nextUrl;
  // Short-circuit public paths BEFORE resolving config. /api/auth/config is
  // public, so the config fetch above never re-enters this gate (no recursion).
  if (isPublicPath(pathname)) return NextResponse.next();

  // Loopback daemon ticks bypass the redirect; the route handler does the real
  // X-Inproc-Token verification (see INPROC_TICK_PATHS note above).
  if (isInprocTick(request)) return NextResponse.next();

  const cfg = await resolveAuthConfig(request);
  if ((cfg.authMode || 'local') !== 'supabase') return NextResponse.next();

  let response = NextResponse.next({ request });
  const supabaseUrl = cfg.supabaseUrl;
  const supabaseAnonKey = cfg.supabaseAnonKey;

  if (!supabaseUrl || !supabaseAnonKey) {
    const loginUrl = request.nextUrl.clone();
    loginUrl.pathname = '/login';
    loginUrl.search = `?error=missing_supabase_env&next=${encodeURIComponent(safeNext(pathname, search))}`;
    return NextResponse.redirect(loginUrl);
  }

  const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
    cookies: {
      getAll() {
        return request.cookies.getAll();
      },
      setAll(cookiesToSet) {
        cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
        response = NextResponse.next({ request });
        cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
      },
    },
  });

  // getUser() is unguarded on the gate for EVERY matched route. A rejection
  // (transient Supabase Auth outage, token-refresh error, rate limit, cold
  // project, network/TLS blip, or a hung connection) must NOT throw — an
  // uncaught middleware error 500s whatever request is in flight (pages, /api,
  // and SSE streams alike, dropping them as CONNECTION_REFUSED). Fail CLOSED:
  // bounce to /login, exactly like the unauthenticated branch. The timeout race
  // keeps a hung Auth endpoint from wedging the request.
  let user = null;
  try {
    const result = await Promise.race([
      supabase.auth.getUser(),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('getUser timed out')), 4000)
      ),
    ]);
    user = result?.data?.user || null;
  } catch (err) {
    console.error('[middleware] supabase.auth.getUser() failed:', err?.message);
    const loginUrl = request.nextUrl.clone();
    loginUrl.pathname = '/login';
    loginUrl.search = `?error=auth_unavailable&next=${encodeURIComponent(safeNext(pathname, search))}`;
    return NextResponse.redirect(loginUrl);
  }

  if (!user) {
    const loginUrl = request.nextUrl.clone();
    loginUrl.pathname = '/login';
    loginUrl.search = `?next=${encodeURIComponent(safeNext(pathname, search))}`;
    return NextResponse.redirect(loginUrl);
  }

  return response;
}

export const config = {
  matcher: ['/((?!_next/static|_next/image).*)'],
};
