import { Injectable, ResolvedReflectiveFactory } from "@angular/core";
import { ServerRestApiService } from "../services/server-rest-api.service";
import { base64 } from "rfc4648";
import { CookieOptions, CookieService } from "ngx-cookie";
import {
  AbstractPrivateKeyring,
  PrivateKeyringV1,
  PRIVATE_KEYRING_CLASSES,
} from "../crypto/keyring/private";
import {
  create_account_login_keyring,
  create_account_login_keyring_self,
  create_account_login_private_keyring,
  create_account_login_request_params,
  create_account_login_session,
} from "../crypto/auth/login";
import {
  account_register_keyring,
  account_register_prepare,
  create_account_register_confirm_request,
  create_account_register_keyring_request,
  create_account_register_prepare_request,
} from "../crypto/auth/register";
import {
  account_password_change,
  create_account_password_change_request,
} from "../crypto/auth/password";
import { SelfAccountKeyring } from "../crypto/keyring/account_self";
import { concatByteArray } from "../crypto/utility";
import { HKDFKeyGeneratorV1 } from "../crypto/base/key/kdf";
import msgpack from "msgpack-lite";
import { CacheService } from "../services/cache.service";
import { editPasswordQuery, sessionEncryptionKeyQuery } from "./querys";
import { AbstractSelfAccountKeyring } from "../crypto/keyring/account_base";
import { DialogService } from "../services/dialog.service";
import { marker } from "@biesbjerg/ngx-translate-extract-marker";
import { USE_PASSWORD_NORMALIZATION } from "../consts/upgrade";

@Injectable({
  providedIn: "root",
})
export class AuthService {
  private userCryptoData: any = null;

  private selfKeyring; // stored self keyring after login

  private readonly SELF_KR = "self_kr";
  private readonly CLIENT_SESSION_KEY = "client_session_key";
  private readonly SELF_KR_STORAGE_KEY = "self_keyring";

  private onLoginSubscriptions: Function[] = [];
  private onLogoutSubscriptions: Function[] = [];

  private selfKeyringLoadedCallback: Function[] = [];

  private anonym: boolean = null;

  constructor(
    private serverRestApiService: ServerRestApiService,
    private cookieService: CookieService,
    private cacheService: CacheService,
    private dialogService: DialogService
  ) {
    console.log("conn :1 - setup user during init");

    if (this.hasAllLoginCookieData()) {
      this.resume()
        .then(() => {
          this.selfKeyringLoadedCallback.forEach((cb) => {
            cb(this.selfKeyring);
          });
        })
        .catch((e) => {
          console.error(
            "conn :2 - user setup fail - forced anonym state is active",
            e
          );
          this.setupAnonymState();
        });
    } else {
      console.log("conn :2 - anonym state is active after init");
      this.setupAnonymState();
    }

    this.serverRestApiService.subscribeOnAuthCloseSubscribe(() => {
      console.log("on-auth-close");
      this.dialogService.showSnackbar(
        marker("Your session was closed. You've been logged out.")
      );
      this.throwSession();
    });
  }

  private setupAnonymState() {
    this.selfKeyring = null;
    this.anonym = true;
    this.serverRestApiService.closeConnection();
    return this.serverRestApiService.startConnection().then(() => {
      this.selfKeyring = SelfAccountKeyring.generate_anonymous();
      this.selfKeyringLoadedCallback.forEach((cb) => {
        cb();
      });
    });
  }

  public getSelfAccountKeyring(): SelfAccountKeyring {
    return this.selfKeyring;
  }

  public isAnonym(): boolean {
    return this.anonym;
  }

  public onSelfAccountKeyringLoaded(cb) {
    if (this.selfKeyring) {
      cb(this.selfKeyring);
    } else {
      this.selfKeyringLoadedCallback.push(cb);
    }
  }

  /**
   * Subscription what will be called after login
   * @param cb
   */
  public onLogin(cb) {
    this.onLoginSubscriptions.push(cb);
  }

  /**
   * Subscription what will be called after logout
   * @param cb
   */
  public onLogout(cb) {
    this.onLogoutSubscriptions.push(cb);
  }

  private combineServerClientKey(
    serverKey: Uint8Array,
    clientKey: Uint8Array
  ): Promise<Uint8Array> {
    if (!serverKey || serverKey.byteLength < 16) {
      return Promise.reject("length of server key is low");
    }
    if (!clientKey || clientKey.byteLength < 16) {
      return Promise.reject("length of client key is low");
    }

    return HKDFKeyGeneratorV1.new_key(
      64,
      concatByteArray(serverKey, clientKey),
      HKDFKeyGeneratorV1.SALTS[1]
    );
  }

  private saveLocalStorage(
    serverSessionKey: Uint8Array,
    self_kr: SelfAccountKeyring
  ): Promise<void> {
    let clientSessionKey = crypto.getRandomValues(new Uint8Array(32));

    return this.combineServerClientKey(serverSessionKey, clientSessionKey).then(
      (combinedKey) => {
        return self_kr.dump_local_storage(combinedKey).then((dump) => {
          let store = msgpack.encode({
            [this.SELF_KR]: dump,
            [this.CLIENT_SESSION_KEY]: clientSessionKey,
          });

          localStorage.setItem(
            this.SELF_KR_STORAGE_KEY,
            base64.stringify(store)
          );
          return;
        });
      }
    );
  }

  private loadLocalStorage(
    serverSessionKey: Uint8Array
  ): Promise<SelfAccountKeyring> {
    let store = localStorage.getItem(this.SELF_KR_STORAGE_KEY);
    if (store) {
      store = msgpack.decode(base64.parse(store));
      return this.combineServerClientKey(
        serverSessionKey,
        store[this.CLIENT_SESSION_KEY]
      ).then((combinedKey) => {
        return SelfAccountKeyring.load_local_storage(
          store[this.SELF_KR],
          combinedKey
        );
      });
    }
    return Promise.reject("Can not find keyring in localstorage");
  }

  private hasAllLoginCookieData() {
    return (
      this.cookieService.get("k") && this.cookieService.get("k").length > 0
    );
  }

  private getServerSessionKey(): Promise<Uint8Array> {
    return this.serverRestApiService
      .query({
        query: sessionEncryptionKeyQuery,
      })
      .then((res) => {
        return base64.parse(res);
      });
  }

  public resume(): Promise<void> {
    this.selfKeyring = null;
    return this.getServerSessionKey().then((serverSessionKey) => {
      return this.loadLocalStorage(serverSessionKey).then((self_kr) => {
        this.anonym = false;
        console.log("conn :2 - user state is active after init");
        return this.serverRestApiService.startConnection().then(() => {
          this.selfKeyring = self_kr;
        });
      });
    });
  }

  private doLoginProcess(
    private_kr_cls: typeof AbstractPrivateKeyring,
    password: string,
    salt: Uint8Array,
    email: string,
    serverSessionKey: Uint8Array,
    otpSalt?: string,
    tfaCode?: string,
    rememberMe: boolean = false
  ): Promise<{
    success: boolean;
    self_kr?: SelfAccountKeyring;
    serverError?: boolean;
    clientError?: boolean;
    err?: any;
  }> {
    return create_account_login_private_keyring(private_kr_cls, password, salt)
      .then((private_kr) => {
        let request = create_account_login_request_params(
          private_kr,
          email,
          serverSessionKey
        );

        return this.serverRestApiService
          .login(
            request.email,
            request.authentication_key,
            request.session_enc_key,
            otpSalt,
            tfaCode,
            rememberMe
          )
          .then((login_data) => {
            /**
         * account_id: "247Lzvcj22222"
          encrypted_master_key: "ARxGnsiivEyhC+fXfNQii/woB8UK9+oO7EjLX6hqm0fUqcVnSUzmwQ7XUrj5iIgiPA=="
          encrypted_session_key: "pvR4jKvnbVUWy5gPNK2FHDKiV1wI+QbSpdUcnGnkan0oXT6+/X8LfQPyrTI="
          encrypting_secret_ec: "AUEGlnXuPTKPzoZIHOppmm9SSe2OnfUHMp42jXQE5I+jGFgpeFn1MYCAcnPSZtdz7Q=="
          public_key: "1dveVP7Xjm9b1Sgx4zGHcgwpkvnlM0G7uWhAAGuLbSo="
          signing_secret_ec: "AQbCbqmMnftu7p87jHKBjr7JpxbaS0BuxJA/OLEWX+sOrvY00bYx/Z269XKpE/NGIw=="
         */

            [
              "encrypted_master_key",
              "encrypting_secret_ec",
              "signing_secret_ec",
              "encrypted_session_key",
              "public_key",
            ].forEach((k) => {
              login_data[k] = base64.parse(login_data[k]);
            });

            console.log("conn :1 - setup user during login");

            return create_account_login_keyring_self(
              private_kr,
              login_data["account_id"],
              login_data["encrypted_master_key"],
              login_data["encrypting_secret_ec"],
              login_data["signing_secret_ec"]
            )
              .then((self_kr) => {
                return create_account_login_session(
                  self_kr,
                  login_data["encrypted_session_key"],
                  login_data["public_key"]
                )
                  .then((session_key) => {
                    let options: CookieOptions = {
                      storeUnencoded: true,
                      //expires: 315360000 - remember me
                    };
                    if (rememberMe) {
                      options.expires = new Date(
                        new Date().setFullYear(new Date().getFullYear() + 1)
                      );
                      options.sameSite = "strict";
                    }

                    this.cookieService.put(
                      "k",
                      base64.stringify(session_key),
                      options
                    );

                    return this.saveLocalStorage(serverSessionKey, self_kr)
                      .then(() => {
                        this.serverRestApiService.closeConnection();

                        console.log(
                          "conn :2 - user state is active after login"
                        );
                        return this.serverRestApiService
                          .startConnection()
                          .then(() => {
                            return { success: true, self_kr };
                          });
                      })
                      .catch((err) => {
                        return { clientError: true, err, success: false };
                      });
                  })
                  .catch((err) => {
                    return { clientError: true, err, success: false };
                  });
              })
              .catch((err) => {
                return { clientError: true, err, success: false };
              });
          })
          .catch((err) => {
            return this.serverRestApiService.startConnection().then(() => {
              console.log("err", err);
              if (err.status == 400) {
                // Bad Request
                return { success: false, err };
              } else {
                return { serverError: true, err, success: false };
              }
            });
          });
      })
      .catch((err) => {
        return { clientError: true, err, success: false };
      });
  }

  /**
   *
   * @returns if fails or ok it will return a resolve or reject promise but the answer
   * will be LoginResult in every case
   */
  public login(
    email,
    password,
    serverSessionKey?: Uint8Array,
    otpSalt?: string,
    tfaCode?: string,
    rememberMe: boolean = false
  ): Promise<LoginResult> {
    this.selfKeyring = null;

    return new Promise((resolve, reject) => {
      this.serverRestApiService
        .getLoginSalt(email)
        .then((res) => {
          let salt = base64.parse(res["salt"]);
          if (!serverSessionKey) {
            serverSessionKey = crypto.getRandomValues(new Uint8Array(16));
          }

          let normalized_pws =
            AbstractPrivateKeyring.flattened_password_forms_with_cls(
              PRIVATE_KEYRING_CLASSES,
              password
            );

          let trial = 0;

          let tryLogin = () => {
            return this.doLoginProcess(
              normalized_pws[trial][0],
              normalized_pws[trial][1],
              salt,
              email,
              serverSessionKey,
              otpSalt,
              tfaCode,
              rememberMe
            ).then((res) => {
              if (res.success) {
                this.anonym = false;
                this.selfKeyring = res.self_kr;
                this.selfKeyringLoadedCallback.forEach((cb) => {
                  cb(this.selfKeyring);
                });
                this.onLoginSubscriptions.forEach((cb) => {
                  cb();
                });
                resolve(res);
              } else {
                trial++;
                if (trial >= normalized_pws.length) {
                  reject(res);
                } else {
                  tryLogin();
                }
              }
            });
          };

          tryLogin();
        })
        .catch((err) => {
          reject({ serverError: true, err });
        });
    });
  }

  private throwSession() {
    this.selfKeyring = null;
    localStorage.removeItem(this.SELF_KR_STORAGE_KEY);
    this.cookieService.remove("k");
    this.cacheService.resetCache();

    console.log("conn :1 - setup user during logout");
    console.log("conn :2 - anonym state is started after logout");

    return this.setupAnonymState().then(() => {
      this.onLogoutSubscriptions.forEach((cb) => {
        cb();
      });
      return;
    });
  }

  public logout(): Promise<void> {
    this.anonym = true;
    return new Promise((resolve, reject) => {
      this.serverRestApiService.closeConnection();
      this.serverRestApiService
        .logout()
        .then(() => {
          this.throwSession().then(() => {
            resolve();
          });
        })
        .catch((e) => {
          this.dialogService
            .openConfirmDialog(
              marker("Logout error"),
              marker(
                "Could not call the logout properly on server side. Do you want to clear the local session instead?"
              )
            )
            .subscribe((res) => {
              if (res === true) {
                this.throwSession()
                  .then(() => {
                    resolve();
                  })
                  .catch(reject);
              } else {
                reject();
              }
            });
        });
    });
  }

  public register(email, password): Promise<any> {
    return account_register_prepare(
      PrivateKeyringV1,
      password,
      undefined,
      undefined,
      USE_PASSWORD_NORMALIZATION
    ).then(
      ([
        client_salt_input,
        encrypted_master_key,
        hashed_authentication_key,
      ]) => {
        let regParams = create_account_register_prepare_request(
          email,
          client_salt_input,
          encrypted_master_key,
          hashed_authentication_key
        );

        return this.serverRestApiService.register(
          regParams.email,
          regParams.client_salt_input,
          regParams.encrypted_master_key,
          regParams.hashed_authentication_key
        );
      }
    );
  }

  public registerConfirm(token: string, email: string, password: string) {
    return new Promise((resolve, reject) => {
      return this.serverRestApiService
        .registerLoginSalt(token, email)
        .then((res) => {
          let salt = base64.parse(res.salt);
          create_account_login_private_keyring(
            PrivateKeyringV1,
            password,
            salt
          ).then((private_kr) => {
            this.serverRestApiService
              .registerLoginCheck(
                token,
                email,
                base64.stringify(private_kr.auth_key)
              )
              .then((res) => {
                account_register_keyring(
                  private_kr,
                  res.account_id,
                  base64.parse(res.encrypted_master_key)
                ).then((self_kr) => {
                  create_account_register_keyring_request(email, self_kr)
                    .then((create_account_register_keyring_request_result) => {
                      this.serverRestApiService
                        .registerChallenge(
                          token,
                          base64.stringify(private_kr.auth_key),
                          create_account_register_keyring_request_result
                        )
                        .then((challengeRes) => {
                          create_account_register_confirm_request(
                            self_kr,
                            base64.parse(challengeRes.challenge),
                            base64.parse(challengeRes.public_key)
                          ).then((confirmRequest) => {
                            this.serverRestApiService
                              .registerConfirm(confirmRequest)
                              .then((res) => {
                                console.log("ok", res);
                                resolve(true);
                              })
                              .catch((err) => {
                                reject(err);
                                console.log("confirm not ok", err);
                              });
                          });
                        });
                    })
                    .catch((err) => {
                      reject(err);
                      console.log("challenge fail", res);
                    });
                });
              })
              .catch((err) => {
                reject(err);
                console.log("not ok", err);
              });
          });
        })
        .catch((err) => {
          reject(err);
          console.log("not ok", err);
        });
    });
  }

  changePassword(password: string): Promise<object> {
    return account_password_change(
      PrivateKeyringV1,
      this.getSelfAccountKeyring(),
      password,
      USE_PASSWORD_NORMALIZATION
    ).then(
      ([
        client_salt_input,
        encrypted_master_key,
        hashed_authentication_key,
      ]) => {
        return create_account_password_change_request(
          this.getSelfAccountKeyring(),
          client_salt_input,
          encrypted_master_key,
          hashed_authentication_key
        ).then((password_change_request) => {
          if (localStorage.getItem("lang")) {
            password_change_request["locale"] = localStorage.getItem("lang");
          }

          return this.serverRestApiService
            .mutate({
              query: editPasswordQuery,
              variables: password_change_request,
            })
            .then((res) => {
              return res;
            });
        });
      }
    );
  }

  private getMasterKey(): string {
    return base64.stringify(
      this.getSelfAccountKeyring().get_secret_key(
        AbstractSelfAccountKeyring.MASTER
      )
    );
  }

  public createRecoveryKeyFile(): Blob {
    return new Blob(
      [
        "# Backup this file to secure location, where only you have access to it!\r\n# Recovery key:\r\n",
        this.getMasterKey(),
      ],
      {
        type: "text/plain;charset=utf-8",
      }
    );
  }

  public parseRecoveryKeyFile() {}

  public getLoginSessions() {}
}

/**
 * Result of the login method
 */
export interface LoginResult {
  /**
   * check if everything is okay with the server (no connection problem)
   */
  serverError?: boolean;

  /**
   * check if the login was success
   */
  success: boolean;

  /**
   * check if the client fails with the crypto
   */
  clientError?: boolean;

  err?: {
    message;
    stack;
  };
}
