import { Injectable } from '@angular/core';
import { User } from 'src/app/model/auth/user';
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { Apollo, gql } from 'apollo-angular';
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum';
import { KeycloakService } from 'keycloak-angular';
import { catchError, map } from 'rxjs/operators';
import { Logger } from 'src/app/service/logger.service';

const log = new Logger('AuthService');

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  protected anyPermissionForAdminPage = [
    PermissionsEnum.administrator,
    PermissionsEnum.settings,
    PermissionsEnum.apiKeys,
    PermissionsEnum.users,
    PermissionsEnum.roles,
  ];

  user$ = new BehaviorSubject<User | null>(null);

  constructor(
    private apollo: Apollo,
    private keycloakService: KeycloakService
  ) {}

  private requestUser() {
    return this.apollo.query<{ user: User }>({
      query: gql`
        query {
          user {
            id
            username
            email

            roles {
              id
              name
              permissions {
                name
              }
            }
          }
        }
      `,
    });
  }

  private userHasPermission(permission: PermissionsEnum): Observable<boolean> {
    return this.apollo
      .query<{ userHasPermission: boolean }>({
        query: gql`
          query UserHasPermission($permission: String!) {
            userHasPermission(permission: $permission)
          }
        `,
        variables: {
          permission,
        },
      })
      .pipe(map(result => result.data.userHasPermission));
  }

  private userHasAnyPermission(
    permissions: PermissionsEnum[]
  ): Observable<boolean> {
    return this.apollo
      .query<{ userHasAnyPermission: boolean }>({
        query: gql`
          query UserHasAnyPermission($permissions: [String]!) {
            userHasAnyPermission(permissions: $permissions)
          }
        `,
        variables: {
          permissions,
        },
      })
      .pipe(map(result => result.data.userHasAnyPermission));
  }

  loadUser(): Promise<User | null> {
    return new Promise<User | null>((resolve, reject) => {
      if (
        !this.keycloakService.isLoggedIn() ||
        this.keycloakService.isTokenExpired()
      ) {
        this.user$.next(null);
        resolve(null);
        return;
      }

      this.requestUser()
        .pipe(
          catchError(err => {
            reject(err);
            throw err;
          })
        )
        .subscribe(result => {
          log.info('User loaded', result.data.user);
          this.user$.next(result?.data?.user);
          resolve(result?.data?.user);
        });
    });
  }

  login() {
    return this.keycloakService.login();
  }

  logout() {
    log.debug('Logging out');
    this.user$.next(null);
    return this.keycloakService.logout(window.location.origin + '/');
  }

  isLoggedIn(): boolean {
    return (
      this.keycloakService.isLoggedIn() &&
      !this.keycloakService.isTokenExpired() &&
      !!this.user$.value
    );
  }

  async isAdmin(): Promise<boolean> {
    return await this.hasAnyPermissionLazy(this.anyPermissionForAdminPage);
  }

  hasAnyPermission(permissions: PermissionsEnum[]): boolean {
    if (!this.user$.value) return false;

    const userPermissions = this.user$.value.roles
      .map(role => (role.permissions ?? []).map(p => p.name))
      .flat();
    return permissions.every(permission =>
      userPermissions.includes(permission)
    );
  }

  async hasAnyPermissionLazy(permissions: PermissionsEnum[]): Promise<boolean> {
    if (this.user$.value && this.user$.value.roles) {
      return this.hasAnyPermission(permissions);
    }

    if (!this.isLoggedIn()) {
      return false;
    }
    return await firstValueFrom(this.userHasAnyPermission(permissions));
  }

  hasPermission(permission: PermissionsEnum): boolean {
    if (!this.user$.value) return false;

    const permissions = this.user$.value.roles
      .map(role => (role.permissions ?? []).map(p => p.name))
      .flat();
    return permissions.includes(permission);
  }

  async hasPermissionLazy(permission: PermissionsEnum): Promise<boolean> {
    if (this.user$.value && this.user$.value.roles) {
      return this.hasPermission(permission);
    }

    return await firstValueFrom(this.userHasPermission(permission));
  }
}
