import { NextRequest, NextResponse } from 'next/server';
import { AUTH_CONFIG } from './auth-config';
import {
  clearAuthCookies,
  readAccessToken,
  readRefreshToken,
  setAuthCookies,
} from './auth-server';
import type { AuthResponse } from './auth-types';

/**
 * Generic authenticated proxy → backend NestJS API. Used by all
 * non-auth API routes (EMR, appointments, etc.) so we don't repeat
 * token + error handling boilerplate per endpoint.
 *
 * - Forwards JSON bodies untouched
 * - Reads access token from the HttpOnly cookie
 * - Returns 401 when no token is present (no upstream call)
 * - **Auto-refreshes** on a 401 from upstream when a refresh-token cookie
 *   is still valid, then replays the original call once with the new token
 *   so the user never sees a spurious "logged out" mid-session
 * - Mirrors upstream status + JSON body so the client gets accurate
 *   error messages
 */
export async function proxyToBackend(
  req: NextRequest,
  path: string,
  init?: { method?: string; body?: unknown },
): Promise<NextResponse> {
  const token = await readAccessToken();
  if (!token) {
    // No access token at all — try a silent refresh before giving up.
    const refreshed = await trySilentRefresh();
    if (!refreshed) {
      return NextResponse.json({ statusCode: 401, message: 'Not authenticated' }, { status: 401 });
    }
    return doProxy(req, path, init, refreshed);
  }
  return doProxy(req, path, init, token);
}

async function doProxy(
  req: NextRequest,
  path: string,
  init: { method?: string; body?: unknown } | undefined,
  token: string,
  isRetry = false,
): Promise<NextResponse> {
  const method = init?.method ?? 'GET';
  const upstream = await fetch(`${AUTH_CONFIG.apiBaseUrl}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-Forwarded-For': req.headers.get('x-forwarded-for') ?? '',
      'User-Agent': req.headers.get('user-agent') ?? 'sehat-web',
    },
    body:
      init?.body !== undefined
        ? typeof init.body === 'string'
          ? init.body
          : JSON.stringify(init.body)
        : undefined,
    cache: 'no-store',
  }).catch((): null => null);

  if (!upstream) {
    return NextResponse.json(
      { statusCode: 503, message: 'Backend unreachable' },
      { status: 503 },
    );
  }

  // Auto-refresh path: access token expired but refresh might still work.
  // Only retry once to avoid loops if refresh-then-access also 401s.
  if (upstream.status === 401 && !isRetry) {
    const refreshed = await trySilentRefresh();
    if (refreshed) {
      return doProxy(req, path, init, refreshed, true);
    }
  }

  if (upstream.status === 204) return new NextResponse(null, { status: 204 });
  const text = await upstream.text();
  return new NextResponse(text, {
    status: upstream.status,
    headers: { 'Content-Type': 'application/json' },
  });
}

/**
 * Multipart-aware proxy used by file-upload endpoints. Same auto-refresh
 * semantics as the JSON proxy.
 */
export async function proxyMultipartToBackend(
  req: NextRequest,
  path: string,
): Promise<NextResponse> {
  const token = await readAccessToken();
  if (!token) {
    const refreshed = await trySilentRefresh();
    if (!refreshed) {
      return NextResponse.json({ statusCode: 401, message: 'Not authenticated' }, { status: 401 });
    }
    return doMultipart(req, path, refreshed);
  }
  return doMultipart(req, path, token);
}

async function doMultipart(
  req: NextRequest,
  path: string,
  token: string,
  isRetry = false,
): Promise<NextResponse> {
  // Read once — if we need to retry we'd have to re-parse, but FormData
  // can't be re-read so we cache the entries on the first call.
  const inFormData = await req.formData();
  const outFormData = new FormData();
  for (const [k, v] of inFormData.entries()) {
    outFormData.append(k, v as Blob | string);
  }

  const upstream = await fetch(`${AUTH_CONFIG.apiBaseUrl}${path}`, {
    method: req.method,
    headers: {
      Authorization: `Bearer ${token}`,
      'X-Forwarded-For': req.headers.get('x-forwarded-for') ?? '',
      'User-Agent': req.headers.get('user-agent') ?? 'sehat-web',
    },
    body: outFormData,
  }).catch((): null => null);

  if (!upstream) {
    return NextResponse.json(
      { statusCode: 503, message: 'Backend unreachable' },
      { status: 503 },
    );
  }
  // We can't trivially retry multipart because outFormData was already
  // consumed by the fetch. Refresh + tell the client to retry instead.
  if (upstream.status === 401 && !isRetry) {
    await trySilentRefresh();
    // Fall through — return 401 so the client retries (their auth-context
    // will see the new cookie). Multipart re-tries are best-effort.
  }

  if (upstream.status === 204) return new NextResponse(null, { status: 204 });
  const text = await upstream.text();
  return new NextResponse(text, {
    status: upstream.status,
    headers: { 'Content-Type': 'application/json' },
  });
}

/**
 * Server-side silent refresh: takes the refresh cookie, exchanges it for a
 * new access/refresh pair via the backend, writes new cookies, and returns
 * the new access token. Returns null if no refresh cookie exists or the
 * backend rejects it (caller should treat as auth failure).
 */
async function trySilentRefresh(): Promise<string | null> {
  const refreshToken = await readRefreshToken();
  if (!refreshToken) return null;

  const upstream = await fetch(`${AUTH_CONFIG.apiBaseUrl}/auth/refresh`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken }),
  }).catch((): null => null);

  if (!upstream || !upstream.ok) {
    // Refresh itself failed — clear cookies so the next request gets a
    // clean 401 rather than chasing a dead refresh token.
    await clearAuthCookies();
    return null;
  }
  const data = (await upstream.json().catch(() => null)) as AuthResponse | null;
  if (!data || !data.accessToken) {
    await clearAuthCookies();
    return null;
  }
  await setAuthCookies(data);
  return data.accessToken;
}
