import {
  HttpErrorResponse,
  type HttpHandlerFn,
  type HttpInterceptorFn,
  type HttpRequest,
} from '@angular/common/http';
import { Injector, inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { AUTH_BFF_BASE_URL, AUTH_PATH_PREFIX } from './auth.config';
import { AuthService } from './auth.service';

/**
 * HTTP interceptor that keeps the SPA's `AuthService` state in sync
 * with the BFF whenever a 401 leaks through. The session may have
 * been destroyed server-side (absolute-timeout middleware, manual
 * revoke, idle-TTL expiry) while the SPA still thinks the user is
 * `authenticated` — without this, components would keep rendering
 * stale identity until the next manual refresh.
 *
 * Behaviour on 401 from a BFF route:
 *   - call `auth.refresh()` so the next `/me` updates `state` to
 *     `anonymous` (or `error` if Redis is down too).
 *   - rethrow the original error so the call site still observes
 *     its failure and can show its own fallback UI / route guards
 *     can react on the next navigation.
 *
 * **Skips the `/me` endpoint itself.** `AuthService.refresh()` calls
 * `/me`, which legitimately 401s when anonymous. Triggering
 * `refresh()` again on that response would loop indefinitely. The
 * skip target is composed from `AUTH_PATH_PREFIX` so it matches
 * whichever surface this app runs on (`/auth/me` for portal-shell,
 * `/admin/auth/me` for portal-admin).
 *
 * Other 4xx / 5xx pass through untouched — they are domain errors,
 * not session-state signals.
 */
export const bffUnauthorizedInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn,
) => {
  const bffBaseUrl = inject(AUTH_BFF_BASE_URL);
  const pathPrefix = inject(AUTH_PATH_PREFIX);
  const meUrl = `${bffBaseUrl}${pathPrefix}/me`;
  // Inject the parent injector here rather than `AuthService` directly:
  // the bootstrap `/me` round-trip is fired from `AuthService`'s own
  // constructor, so a synchronous `inject(AuthService)` here would
  // re-enter DI while `AuthService` is still being constructed and
  // raise a circular-construction error that swallows the original
  // request before the testing backend can record it. Resolving
  // `AuthService` lazily from `catchError` defers the lookup to a
  // post-construction tick.
  const injector = inject(Injector);

  return next(req).pipe(
    catchError((err: unknown) => {
      if (
        err instanceof HttpErrorResponse &&
        err.status === 401 &&
        req.url.startsWith(bffBaseUrl) &&
        !req.url.startsWith(meUrl)
      ) {
        // Best-effort sync — swallow the refresh-side error so the
        // original 401 is what bubbles up to the caller.
        const auth = injector.get(AuthService);
        void auth.refresh().catch(() => undefined);
      }
      return throwError(() => err);
    }),
  );
};
