import { Injectable } from '@angular/core';
import { CookieOptions } from 'ngx-cookie';
import { base64 } from 'rfc4648';
import { AppStorage } from 'src/app/shared/app-storage';
import { USE_PASSWORD_NORMALIZATION } from '../consts/upgrade';
import { create_account_delete_request } from '../crypto/auth/delete';
import {
  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_password_change,
  create_account_password_change_request,
} from '../crypto/auth/password';
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 { AbstractSelfAccountKeyring } from '../crypto/keyring/account_base';
import { SelfAccountKeyring } from '../crypto/keyring/account_self';
import {
  AbstractPrivateKeyring,
  PrivateKeyringV1,
  PRIVATE_KEYRING_CLASSES,
} from '../crypto/keyring/private';
import { AppCookieService } from '../services/app-cookie.service';
import { BlobService } from '../services/blob.service';
import { CacheService } from '../services/cache/cache.service';
import { DialogService } from '../services/dialog.service';
import { ServerRestApiService } from '../services/server-rest-api.service';
import { SnackBarService } from '../services/snackbar.service';
import { SessionCrypto } from '../session-crypto';
import { chatInputStorageKey } from './chat-service-types';
import {
  deleteAccountPrepareQuery,
  deleteAccountQuery,
  editPasswordQuery,
  sessionEncryptionKeyQuery,
} from './querys';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private selfKeyring; // stored self keyring after login

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

  private selfKeyringLoadedCallback: Function[] = [];

  private anonym: boolean = null;

  private shouldThrowSession: boolean = false;

  constructor(
    private serverRestApiService: ServerRestApiService,
    private cookieService: AppCookieService,
    private cacheService: CacheService,
    private dialogService: DialogService,
    private blobService: BlobService,
    private snackbarService: SnackBarService
  ) {
    console.log('AuthService constructor');

    this.serverRestApiService.subscribeOnWSConnected(() => {
      if (!this.shouldThrowSession) {
        this.setupAuthConnection();
      }
    });

    this.serverRestApiService.subscribeOnWSAuthClosed(() => {
      console.log('throw session');
      this.shouldThrowSession = true;

      this.serverRestApiService.closeConnection();
      this.throwSession().then(() => {
        this.shouldThrowSession = false;
        this.serverRestApiService.startConnection();
      });
    });

    // init websocket and restore auth on onconnected
    this.serverRestApiService.startConnection();
  }

  /*private handleAuthClose() {
    console.log('on-auth-close');
    this.snackbarService.showSnackbar(marker("Your session was closed. You've been logged out."));

    console.log('on-auth-close: 1 - close connection');
    this.serverRestApiService.closeConnection();
    console.log('on-auth-close: 2 - throw session data');
    this.throwSession().then(() => {
      console.log('on-auth-close: 3 - setup anonym state');
      return this.setupAnonymState().then(() => {
        console.log('on-auth-close: 4 - call after init callbacks');
        let cbCopy = [...this.selfKeyringLoadedCallback];
        this.selfKeyringLoadedCallback.forEach((cb) => {
          cb(this.selfKeyring);
        });
        this.onLogoutSubscriptions.forEach((cb) => {
          cb();
        });
        return;
      });
    });
  }*/

  private setupAuthConnection(): Promise<void> {
    console.log('conn: 1 - auth setup start');
    return this.hasAllLoginCookieData()
      .then((hasAllLoginCookieData) => {
        if (hasAllLoginCookieData) {
          console.log('conn: 2 - login session detected, setup user');
          return this.resume().catch((e) => {
            console.log('conn: E - cannot resume session, force anonym state', e);
            return this.throwSession().then(() => {
              return this.setupAnonymState();
            });
          });
        } else {
          console.log('conn: 2 - anonym state is active after init');
          return this.setupAnonymState();
        }
      })
      .then(() => {
        let cbCopy = [...this.selfKeyringLoadedCallback];

        console.log('conn: 3 - connection OK, call after auth init callbacks');
        cbCopy.forEach((cb) => {
          cb(this.selfKeyring);
        });
        return;
      });
  }

  private setupAnonymState(): Promise<void> {
    this.selfKeyring = null;
    this.anonym = true;
    this.selfKeyring = SelfAccountKeyring.generate_anonymous();
    return;
  }

  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);
    }
  }

  public unsubscribeSelfAccountKeyringLoaded(cb) {
    let index = this.selfKeyringLoadedCallback.indexOf(cb);
    if (index > -1) {
      this.selfKeyringLoadedCallback.splice(index, 1);
    }
  }

  /**
   * 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 hasAllLoginCookieData(): Promise<boolean> {
    return this.cookieService.get('k').then((k) => {
      return k && 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 SessionCrypto.loadLocalStorage(serverSessionKey).then((self_kr) => {
          this.anonym = false;
          this.selfKeyring = self_kr;
          return;
        });
      })
      .catch((err) => {
        if (err.message == 'Unauthenticated!') {
          this.shouldThrowSession = true;

          this.serverRestApiService.closeConnection();
          this.throwSession().then(() => {
            this.shouldThrowSession = false;
            this.serverRestApiService.startConnection();
          });
        } else {
          console.log('resume restart');
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              this.resume().then(resolve);
            }, 1000);
          });
        }
      });
  }

  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);

        console.log('login: 1 - call login endpoint');
        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('login: 2 - request OK, setup user');

            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';
                    }

                    return this.cookieService
                      .put('k', base64.stringify(session_key), options)
                      .then(() => {
                        return SessionCrypto.saveLocalStorage(serverSessionKey, self_kr)
                          .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> {
    const oldKeyring = this.selfKeyring;
    this.selfKeyring = null;

    return new Promise((resolve, reject) => {
      console.log('login: 0 - get salt');
      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;

                console.log('login: 3 - login ok, reopen connection');
                this.serverRestApiService.closeConnection();

                this.serverRestApiService.startConnection().then(() => {
                  console.log('login: operation DONE; call after callbacks');

                  /*
                  let cbCopy = [...this.selfKeyringLoadedCallback]
                  cbCopy.forEach((cb) => {
                    cb(this.selfKeyring);
                  });*/
                  let cbCopy = [...this.onLoginSubscriptions];
                  cbCopy.forEach((cb) => {
                    cb();
                  });
                  resolve(res);
                });
              } else {
                trial++;
                if (trial >= normalized_pws.length) {
                  this.selfKeyring = oldKeyring;
                  reject(res);
                } else {
                  return tryLogin();
                }
              }
            });
          };

          console.log('login: 0+ salt OK, start login');
          return tryLogin();
        })
        .catch((err) => {
          this.selfKeyring = oldKeyring;
          reject({ serverError: true, err });
        });
    });
  }

  private throwSession() {
    this.selfKeyring = null;
    AppStorage.removeItem(SessionCrypto.SELF_KR_STORAGE_KEY);
    AppStorage.removeItemWithPrefix(chatInputStorageKey);
    return this.cookieService.remove('k').then(() => {
      this.cacheService.resetCache();
      this.onLogoutSubscriptions.forEach((cb) => {
        cb();
      });
    });
  }

  public logout(): Promise<void> {
    return this.serverRestApiService.logout();
  }

  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 (AppStorage.getItem('lang')) {
          password_change_request['locale'] = AppStorage.getItem('lang');
        }

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

  public deleteAccount(): Promise<object> {
    return this.serverRestApiService
      .mutate({
        query: deleteAccountPrepareQuery,
      })
      .then((delete_token_result) => {
        const delete_token = new TextEncoder().encode(delete_token_result);
        return create_account_delete_request(this.getSelfAccountKeyring(), delete_token).then(
          (delete_account_request) => {
            return this.serverRestApiService
              .mutate({
                query: deleteAccountQuery,
                variables: { ...delete_account_request, deleteToken: delete_token_result },
              })
              .then((deleteAccountQueryResult) => {
                return deleteAccountQueryResult;
              });
          }
        );
      });
  }

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

  public createRecoveryKeyFile(): Blob {
    return this.blobService.new([
      '# 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;
  };
}
