import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StorageMap } from '@ngx-pwa/local-storage';
import { Apollo, gql } from 'apollo-angular';
import { User } from '@prisma/client';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private userSubject?: BehaviorSubject<User>;

  constructor(
    private readonly $storage: StorageMap,
    private readonly $apollo: Apollo
  ) {}

  /**
   * Get the userSubject as an observable or return
   * undefined if no user can be loaded.
   *
   * Attempts to retrieve the User object from IndexedDB
   * storage if the 'user' key is present. If the 'user'
   * key is not present but an 'accessToken' is, attempts
   * to load user data from the GraphQL API.
   *
   * If all else fails, the Observable returns with an
   * undefined value.
   *
   * @returns Observable<User> | undefined
   */
  public user(): Observable<User | undefined> {
    return !this.userSubject
      ? this.$storage.has('user').pipe(
          switchMap((hasUser: boolean) => {
            return hasUser
              ? this.$storage.get('user').pipe(map((user) => user as User))
              : of(false);
          }),
          switchMap((user: User | boolean) => {
            return !user
              ? this.$storage.has('accessToken').pipe(
                  switchMap((hasToken: boolean) => {
                    return hasToken
                      ? this.$apollo.query({
                          query: gql(`
                      {
                        user {
                          id
                          email
                          firstName
                          lastName
                          role
                        }
                      }
                    `)
                        })
                      : of(false);
                  }),
                  map((result) => {
                    if (typeof result === 'boolean') {
                      return result;
                    } else {
                      const { data, error } = result;

                      if (error) {
                        throw error;
                      }

                      this.$storage.set('user', (data as any).user);

                      return (data as any).user;
                    }
                  })
                )
              : of(user);
          }),
          switchMap((user) => {
            this.userSubject = user
              ? new BehaviorSubject(user)
              : new BehaviorSubject(undefined as unknown as User);
            return this.userSubject?.asObservable();
          })
        )
      : this.userSubject.asObservable();
  }

  public update(input: User) {
    this.userSubject?.next(input);
  }

  public logout() {
    this.userSubject?.next(undefined as unknown as User);
    this.$storage.delete('user').subscribe(() => {});
    this.$storage.delete('accessToken').subscribe(() => {});
  }
}
