import { inject, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { environment } from 'environments/environment';
import { finalize, firstValueFrom } from 'rxjs';
import { AuthenticationService } from '../../authentication/shared/authentication.service';
import { TokenStore } from '../../authentication/shared/token-store.service';
import type { GraphQLAuthHandler } from '../graphql.types';

// Operations authenticated as the CLIENT (HTTP Basic, RFC 6749 §2.3.1 / RFC 7009 §2.1) rather
// than as the user (Bearer): login (no session yet) and logout/revocation (client-authenticated).
// Exported so the Apollo link's refresh scope guard shares one source of truth.
export const BASIC_AUTH_OPERATIONS = ['oAuthCreateToken', 'oAuthRevokeToken'];

// Shared Basic-credentials header for the client-authenticated operations. Used by both the
// DI-less default and the DI-aware factory below so the logic lives in one place.
function basicAuthorizationHeader(operationName: string | null): string | null {
  if (operationName && BASIC_AUTH_OPERATIONS.includes(operationName)) {
    return `Basic ${btoa(environment.oAuth.applicationCode + ':' + environment.oAuth.applicationSecret)}`;
  }
  return null;
}

/**
 * DI-less default handler bound to the GRAPHQL_AUTH_HANDLER token's `factory`. It cannot reach
 * AuthenticationService, so it can never refresh (`tryRefresh` → false) and its `handleUnauthorized`
 * is a no-op warn. The real, DI-aware handler is provided by `provideApollo()` via
 * `auroraGraphqlAuthHandlerFactory`; this default only applies if the token is used without it.
 */
export const auroraGraphqlAuthHandler: GraphQLAuthHandler = {
  async getAuthorizationHeader(operationName: string | null): Promise<string | null> {
    return basicAuthorizationHeader(operationName);
  },
  tryRefresh(): Promise<boolean> {
    return Promise.resolve(false);
  },
  handleUnauthorized(): void {
    console.warn('[GraphQL] Unauthorized request — no auth handler configured');
  },
};

/**
 * DI-aware GraphQL auth handler — the framework default wired by `provideApollo()`.
 *
 * It resolves `AuthenticationService` / `Router` LAZILY through the captured `Injector` (only when
 * a refresh or sign-out is actually needed), so the Apollo client never acquires a construction-time
 * dependency on `AuthenticationService` — avoiding the `Apollo ↔ AuthenticationService ↔ Apollo`
 * cycle the `TokenStore` split exists to prevent. `TokenStore` has zero Apollo dependency, so it is
 * safe to inject eagerly. Lives beside the port/default here (not in `authentication`) because
 * `graphql` already depends on `authentication`, never the reverse.
 */
export function auroraGraphqlAuthHandlerFactory(): GraphQLAuthHandler {
  const injector = inject(Injector);
  const tokenStore = inject(TokenStore);

  // Single-flight refresh: one shared in-flight promise. Concurrent access-expiry 401s await the
  // same promise and all retry once it settles; cleared on settle so a later expiry refreshes anew.
  let inFlightRefresh: Promise<boolean> | null = null;

  // Coalesce concurrent sign-outs: N operations failing the refresh at once must trigger ONE
  // signOut + redirect, not N (which would fire N oAuthRevokeToken mutations + N navigations).
  // Reset on settle so a later session can still sign out.
  let signingOut = false;

  return {
    async getAuthorizationHeader(operationName: string | null): Promise<string | null> {
      return basicAuthorizationHeader(operationName);
    },

    tryRefresh(): Promise<boolean> {
      // No refresh token → nothing to refresh; route straight to sign-out.
      if (!tokenStore.refreshToken()) {
        return Promise.resolve(false);
      }

      inFlightRefresh ??= firstValueFrom(
        injector.get(AuthenticationService).signInUsingRefreshToken(),
      )
        // signInUsingRefreshToken() already maps a rejected refresh to `false` (and clears
        // credentials); this catch is a defensive backstop.
        .catch(() => false)
        .finally(() => {
          inFlightRefresh = null;
        });

      return inFlightRefresh;
    },

    handleUnauthorized(): void {
      // Refresh failed or unavailable: end the session and bounce to sign-in. signOut() guarantees
      // a local credential clear (best-effort server revoke via finalize); navigate regardless.
      // Coalesce concurrent callers (N failed ops) into a single signOut + redirect.
      if (signingOut) return;
      signingOut = true;
      const router = injector.get(Router);
      injector
        .get(AuthenticationService)
        .signOut()
        .pipe(finalize(() => (signingOut = false)))
        .subscribe({
          next: () => void router.navigate(['/auth/sign-in']),
          error: () => void router.navigate(['/auth/sign-in']),
        });
    },
  };
}
