/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { firstValueFrom, Observable, Subscription, timer } from "rxjs";
import { AppSettings } from "../../app-setting";
import { Role, User } from "../../type/user";
import { ignoreElements } from "rxjs/operators";
import { JwtHelperService } from "@auth0/angular-jwt";
import { NotificationService } from "../notification/notification.service";
import { GmailService } from "../gmail/gmail.service";
import { GuiConfigService } from "../gui-config.service";
import { NzModalService } from "ng-zorro-antd/modal";
import { RegistrationRequestModalComponent } from "./registration-request-modal/registration-request-modal.component";

export const TOKEN_KEY = "access_token";

/**
 * User Service contains the function of registering and logging the user.
 * It will save the user account inside for future use.
 *
 * @author Adam
 */
@Injectable({
  providedIn: "root",
})
export class AuthService {
  public static readonly LOGIN_ENDPOINT = "auth/login";
  public static readonly REFRESH_TOKEN = "auth/refresh";
  public static readonly REGISTER_ENDPOINT = "auth/register";
  public static readonly GOOGLE_LOGIN_ENDPOINT = "auth/google/login";

  private tokenExpirationSubscription?: Subscription;

  constructor(
    private http: HttpClient,
    private jwtHelperService: JwtHelperService,
    private notificationService: NotificationService,
    private gmailService: GmailService,
    private config: GuiConfigService,
    private modal: NzModalService
  ) {}

  /**
   * This method will handle the request for user registration.
   * It will automatically login, save the user account inside and trigger userChangeEvent when success
   * @param username
   * @param password
   */
  public register(username: string, password: string): Observable<Readonly<{ accessToken: string }>> {
    return this.http.post<Readonly<{ accessToken: string }>>(
      `${AppSettings.getApiEndpoint()}/${AuthService.REGISTER_ENDPOINT}`,
      {
        username,
        password,
      }
    );
  }

  /**
   * This method will handle the request for Google login.
   * It will automatically login, save the user account inside and trigger userChangeEvent when success

   */
  public googleAuth(credential: string): Observable<Readonly<{ accessToken: string }>> {
    return this.http.post<Readonly<{ accessToken: string }>>(
      `${AppSettings.getApiEndpoint()}/${AuthService.GOOGLE_LOGIN_ENDPOINT}`,
      credential,
      {
        headers: {
          "Content-Type": "text/plain",
          Accept: "application/json",
        },
      }
    );
  }

  /**
   * This method will handle the request for user login.
   * It will automatically login, save the user account inside and trigger userChangeEvent when success
   * @param username
   * @param password
   */
  public auth(username: string, password: string): Observable<Readonly<{ accessToken: string }>> {
    return this.http.post<Readonly<{ accessToken: string }>>(
      `${AppSettings.getApiEndpoint()}/${AuthService.LOGIN_ENDPOINT}`,
      { username, password }
    );
  }

  /**
   * this method will clear the saved user account and trigger userChangeEvent
   */
  public logout(): undefined {
    AuthService.removeAccessToken();
    this.tokenExpirationSubscription?.unsubscribe();
    return undefined;
  }

  public loginWithExistingToken(): User | undefined {
    this.tokenExpirationSubscription?.unsubscribe();
    const token = AuthService.getAccessToken();

    if (token == null) {
      return this.logout();
    }

    if (this.jwtHelperService.isTokenExpired(token)) {
      this.notificationService.error("Access token is expired!");
      return this.logout();
    }

    const role = this.jwtHelperService.decodeToken(token).role;
    const uid = this.jwtHelperService.decodeToken(token).userId;
    const email = this.jwtHelperService.decodeToken(token).email;
    const name = this.jwtHelperService.decodeToken(token).sub;

    if (this.config.env.inviteOnly && role === Role.INACTIVE) {
      this.checkRegistrationRequired(uid).subscribe(required => {
        if (required) {
          this.openRegistrationModal(uid, email, name);
        } else {
          this.modal.info({
            nzTitle: "Access Pending",
            nzContent: `
            Your account is still inactive, and we already received your request.
            Please wait for an admin to approve your access.
          `,
            nzOkText: "OK",
            nzMaskClosable: false,
            nzClosable: false,
            nzOnOk: () => {
              this.logout();
              return true;
            },
          });
        }
      });

      return this.logout();
    }

    this.registerAutoLogout();
    return {
      uid: this.jwtHelperService.decodeToken(token).userId,
      name: this.jwtHelperService.decodeToken(token).sub,
      email: email,
      googleId: this.jwtHelperService.decodeToken(token).googleId,
      googleAvatar: this.jwtHelperService.decodeToken(token).googleAvatar,
      role: role,
      comment: this.jwtHelperService.decodeToken(token).comment,
      joiningReason: this.jwtHelperService.decodeToken(token).joiningReason,
    };
  }

  private registerAutoLogout() {
    this.tokenExpirationSubscription?.unsubscribe();
    const expirationTime = this.jwtHelperService.getTokenExpirationDate()?.getTime();
    const token = AuthService.getAccessToken();
    if (token !== null && !this.jwtHelperService.isTokenExpired(token) && expirationTime !== undefined) {
      // In RxJS 7, timer emits immediately then completes. Using ignoreElements() suppresses
      // the emitted value so the complete callback fires only after the specified delay.
      this.tokenExpirationSubscription = timer(expirationTime - new Date().getTime())
        .pipe(ignoreElements())
        .subscribe({ complete: () => this.logout() });
    }
  }

  static setAccessToken(token: string): void {
    localStorage.setItem(TOKEN_KEY, token);
  }

  static getAccessToken(): string | null {
    return localStorage.getItem(TOKEN_KEY);
  }

  static removeAccessToken(): void {
    localStorage.removeItem(TOKEN_KEY);
  }

  /**
   * Returns true if the system needs to prompt the user with the registration form
   * @param uid
   * @private
   */
  private checkRegistrationRequired(uid: number): Observable<boolean> {
    return this.http.get<boolean>(`${AppSettings.getApiEndpoint()}/user/joining-reason/required`, {
      params: { uid: uid.toString() },
    });
  }

  /**
   * Submits changes to the backend with affiliation and joining reason
   * @param uid
   * @param affiliation
   * @param reason
   * @private
   */
  private submitRegistration(uid: number, affiliation: string, reason: string): Observable<void> {
    return this.http.put<void>(`${AppSettings.getApiEndpoint()}/user/joining-reason`, {
      uid,
      affiliation,
      joiningReason: reason,
    });
  }

  /**
   * Opens the registration modal (registration request modal)
   * @param uid
   * @param email
   * @param defaultName
   * @private
   */
  private openRegistrationModal(uid: number, email: string, defaultName: string): void {
    const modalRef = this.modal.create<RegistrationRequestModalComponent>({
      nzContent: RegistrationRequestModalComponent,
      nzData: { uid, email, name: defaultName },
      nzOkText: "Send request to Admin",
      nzCancelText: "Cancel",
      nzMaskClosable: false,
      nzClosable: false,

      nzOnOk: async () => {
        const comp = modalRef.getContentComponent();
        const { affiliation, reason } = comp.getValues();

        if (!reason) {
          this.notificationService.error("Reason is required");
          return false;
        }

        try {
          await firstValueFrom(this.submitRegistration(uid, affiliation, reason));
          this.gmailService.notifyUnauthorizedLogin(email, affiliation, reason);
        } finally {
          this.logout();
        }
        return true;
      },

      nzOnCancel: () => this.logout(),
    });

    const comp = modalRef.getContentComponent();
    modalRef.updateConfig({
      nzTitle: comp.modalTitle,
    });
  }
}
