import { inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Apollo } from 'apollo-angular';
import { catchError, finalize, first, map, Observable, of, switchMap, throwError } from 'rxjs';
import { oAuthCreateToken, oAuthRevokeToken } from '../../data-access/authentication.graphql';
import { isTokenExpired } from '../authentication';
import { AuthenticationService } from '../authentication.service';
import { ClientGrantType, type OAuthTokens } from '../authentication.types';
import { TokenStore } from '../token-store.service';

@Injectable()
export class AuthenticationAuroraAdapterService extends AuthenticationService {
  private readonly apollo = inject(Apollo);
  private readonly router = inject(Router);
  private readonly tokenStore = inject(TokenStore);

  signIn(credentials: { email: string; password: string }): Observable<boolean> {
    if (this.tokenStore.authenticated()) {
      return throwError(() => new Error('User is already logged in.'));
    }

    return this.apollo
      .mutate<{ oAuthCreateToken: OAuthTokens }>({
        mutation: oAuthCreateToken,
        variables: {
          payload: {
            username: credentials.email,
            password: credentials.password,
            grantType: ClientGrantType.PASSWORD,
          },
        },
        // Suppress the global toast for invalid_grant: the login form renders the error inline.
        context: { skipGlobalErrors: ['invalid_grant'] },
      })
      .pipe(
        switchMap((response) => {
          const next = response.data?.oAuthCreateToken;
          if (next) this.tokenStore.setCredentials(next);
          return of(true);
        }),
      );
  }

  signInUsingRefreshToken(): Observable<boolean> {
    return this.apollo
      .mutate<{ oAuthCreateToken: OAuthTokens }>({
        mutation: oAuthCreateToken,
        variables: {
          payload: {
            refreshToken: this.tokenStore.refreshToken(),
            grantType: ClientGrantType.REFRESH_TOKEN,
          },
        },
        // Suppress the global toast for invalid_grant on refresh failures: this call
        // runs transparently mid-session (triggered by the auth guard or apollo link retry),
        // so an expired/revoked refresh token should NOT surface a confusing toast — the
        // catchError below handles it by clearing credentials and returning false.
        context: { skipGlobalErrors: ['invalid_grant'] },
      })
      .pipe(
        first(),
        switchMap((response) => {
          const next = response.data?.oAuthCreateToken;
          if (next) this.tokenStore.setCredentials(next);
          return of(true);
        }),
        catchError(() => {
          // refresh token rejected by backend, clear stale credentials
          this.tokenStore.clear();
          return of(false);
        }),
      );
  }

  signOut(): Observable<boolean> {
    const refreshToken = this.tokenStore.refreshToken();

    // No session to revoke server-side — just clear locally.
    if (!refreshToken) {
      this.tokenStore.clear();
      return of(true);
    }

    // Best-effort server-side revocation (RFC 7009). The refresh token (read BEFORE clearing)
    // travels in the payload; the client Basic credentials are attached per-operation by
    // `auroraGraphqlAuthHandler`. Local credentials are cleared regardless of the outcome (finalize)
    // so a network/server failure never leaves the user in a logged-in client state.
    return this.apollo
      .mutate<{ oAuthRevokeToken: boolean }>({
        mutation: oAuthRevokeToken,
        variables: { payload: { token: refreshToken } },
      })
      .pipe(
        catchError(() => of(null)),
        finalize(() => this.tokenStore.clear()),
        map(() => true),
      );
  }

  signUp(user: { name: string; email: string; password: string }): Observable<boolean> {
    // TODO: implement with actual mutation
    return of(true);
  }

  forgotPassword(email: string, origin: string = window.location.origin): Observable<boolean> {
    // TODO: implement with actual iamForgotPasswordUserMutation
    return of(true);
  }

  resetPassword(password: string, token: string): Observable<boolean> {
    // TODO: implement with actual iamResetPasswordUserMutation
    return of(true);
  }

  check(): Observable<boolean> {
    const access = this.tokenStore.accessToken();
    const refresh = this.tokenStore.refreshToken();

    if (!access || !refresh) {
      this.tokenStore.clear();
      return of(false);
    }

    // The refresh token is OPAQUE (RFC 6749 — random bytes, not a JWT), so its expiry
    // cannot be inspected client-side; only the backend knows it (stored in DB). We do NOT
    // run isTokenExpired() on it. If the access token is expired we attempt a refresh: the
    // backend validates the refresh token and answers invalid_grant if it is expired/revoked,
    // which signInUsingRefreshToken() turns into clear() + of(false).
    if (isTokenExpired(access)) {
      return this.signInUsingRefreshToken();
    }

    return of(true);
  }
}
