import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { environment } from '@src/environments/environment';
import { first } from 'rxjs/operators';
import { ArgPair, Envelope, Phone, Syncable, User } from 'cic-client-meta';
import { AccountDetails } from '@app/_models';
import { LoggingService } from '@app/_services/logging.service';
import { TokenService } from '@app/_services/token.service';
import { AccountIndex } from '@app/_eth';
import { MutableKeyStore, PGPSigner, Signer } from '@app/_pgp';
import { RegistryService } from '@app/_services/registry.service';
import { CICRegistry } from 'cic-client';
import { AuthService } from '@app/_services/auth.service';
import { personValidation, vcardValidation } from '@app/_helpers';
import { add0x } from '@src/assets/js/ethtx/dist/hex';
const vCard = require('vcard-parser');

@Injectable({
  providedIn: 'root',
})
export class UserService {
  headers: HttpHeaders = new HttpHeaders({ 'x-cic-automerge': 'client' });
  keystore: MutableKeyStore;
  signer: Signer;
  registry: CICRegistry;

  accounts: Array<AccountDetails> = [];
  private accountsList: BehaviorSubject<Array<AccountDetails>> = new BehaviorSubject<
    Array<AccountDetails>
  >(this.accounts);
  accountsSubject: Observable<Array<AccountDetails>> = this.accountsList.asObservable();

  actions: Array<any> = [];
  private actionsList: BehaviorSubject<any> = new BehaviorSubject<any>(this.actions);
  actionsSubject: Observable<Array<any>> = this.actionsList.asObservable();

  constructor(
    private httpClient: HttpClient,
    private loggingService: LoggingService,
    private tokenService: TokenService,
    private registryService: RegistryService,
    private authService: AuthService
  ) {
    this.authService.init().then(() => {
      this.keystore = authService.mutableKeyStore;
      this.signer = new PGPSigner(this.keystore);
    });
    this.registry = registryService.getRegistry();
    this.registry.load();
  }

  resetPin(phone: string): Observable<any> {
    const params: HttpParams = new HttpParams().set('phoneNumber', phone);
    return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params });
  }

  getAccountStatus(phone: string): Observable<any> {
    const params: HttpParams = new HttpParams().set('phoneNumber', phone);
    return this.httpClient.get(`${environment.cicUssdUrl}/pin`, { params });
  }

  getLockedAccounts(offset: number, limit: number): Observable<any> {
    return this.httpClient.get(`${environment.cicUssdUrl}/accounts/locked/${offset}/${limit}`);
  }

  async changeAccountInfo(
    address: string,
    name: string,
    phoneNumber: string,
    age: string,
    type: string,
    bio: string,
    gender: string,
    businessCategory: string,
    userLocation: string,
    location: string,
    locationType: string
  ): Promise<any> {
    const accountInfo: any = {
      vcard: {
        fn: [{}],
        n: [{}],
        tel: [{}],
      },
      location: {},
    };
    accountInfo.vcard.fn[0].value = name;
    accountInfo.vcard.n[0].value = name.split(' ');
    accountInfo.vcard.tel[0].value = phoneNumber;
    accountInfo.products = [bio];
    accountInfo.gender = gender;
    accountInfo.age = age;
    accountInfo.type = type;
    accountInfo.category = businessCategory;
    accountInfo.location.area = location;
    accountInfo.location.area_name = userLocation;
    accountInfo.location.area_type = locationType;
    await vcardValidation(accountInfo.vcard);
    accountInfo.vcard = btoa(vCard.generate(accountInfo.vcard));
    const accountKey: string = await User.toKey(address);
    this.getAccountDetailsFromMeta(accountKey)
      .pipe(first())
      .subscribe(
        async (res) => {
          const syncableAccount: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
          const update: Array<ArgPair> = [];
          for (const prop of Object.keys(accountInfo)) {
            update.push(new ArgPair(prop, accountInfo[prop]));
          }
          syncableAccount.update(update, 'client-branch');
          await personValidation(syncableAccount.m.data);
          await this.updateMeta(syncableAccount, accountKey, this.headers);
        },
        async (error) => {
          this.loggingService.sendErrorLevelMessage(
            'Cannot find account info in meta service',
            this,
            { error }
          );
          const syncableAccount: Syncable = new Syncable(accountKey, accountInfo);
          await this.updateMeta(syncableAccount, accountKey, this.headers);
        }
      );
    return accountKey;
  }

  async updateMeta(
    syncableAccount: Syncable,
    accountKey: string,
    headers: HttpHeaders
  ): Promise<any> {
    const envelope: Envelope = await this.wrap(syncableAccount, this.signer);
    const reqBody: string = envelope.toJSON();
    this.httpClient
      .put(`${environment.cicMetaUrl}/${accountKey}`, reqBody, { headers })
      .pipe(first())
      .subscribe((res) => {
        this.loggingService.sendInfoLevelMessage(`Response: ${res}`);
      });
  }

  getActions(): void {
    this.httpClient
      .get(`${environment.cicCacheUrl}/actions`)
      .pipe(first())
      .subscribe((res) => this.actionsList.next(res));
  }

  getActionById(id: string): Observable<any> {
    return this.httpClient.get(`${environment.cicCacheUrl}/actions/${id}`);
  }

  approveAction(id: string): Observable<any> {
    return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: true });
  }

  revokeAction(id: string): Observable<any> {
    return this.httpClient.post(`${environment.cicCacheUrl}/actions/${id}`, { approval: false });
  }

  getAccountDetailsFromMeta(userKey: string): Observable<any> {
    return this.httpClient.get(`${environment.cicMetaUrl}/${userKey}`, { headers: this.headers });
  }

  wrap(syncable: Syncable, signer: Signer): Promise<Envelope> {
    return new Promise<Envelope>(async (resolve, reject) => {
      syncable.setSigner(signer);
      syncable.onwrap = async (env) => {
        if (env === undefined) {
          reject();
          return;
        }
        resolve(env);
      };
      await syncable.sign();
    });
  }

  async loadAccounts(limit: number = 100, offset: number = 0): Promise<void> {
    this.resetAccountsList();
    const accountIndexAddress: string = await this.registry.getContractAddressByName(
      'AccountRegistry'
    );
    const accountIndexQuery = new AccountIndex(accountIndexAddress);
    const accountAddresses: Array<string> = await accountIndexQuery.last(
      await accountIndexQuery.totalAccounts()
    );
    this.loggingService.sendInfoLevelMessage(accountAddresses);
    for (const accountAddress of accountAddresses.slice(offset, offset + limit)) {
      await this.getAccountByAddress(accountAddress, limit);
    }
  }

  async getAccountByAddress(
    accountAddress: string,
    limit: number = 100
  ): Promise<Observable<AccountDetails>> {
    const accountSubject: Subject<any> = new Subject<any>();
    this.getAccountDetailsFromMeta(await User.toKey(add0x(accountAddress)))
      .pipe(first())
      .subscribe(async (res) => {
        const account: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
        const accountInfo = account.m.data;
        await personValidation(accountInfo);
        accountInfo.balance = await this.tokenService.getTokenBalance(
          accountInfo.identities.evm[`bloxberg:${environment.bloxbergChainId}`][0]
        );
        accountInfo.vcard = vCard.parse(atob(accountInfo.vcard));
        await vcardValidation(accountInfo.vcard);
        this.accounts.unshift(accountInfo);
        if (this.accounts.length > limit) {
          this.accounts.length = limit;
        }
        this.accountsList.next(this.accounts);
        accountSubject.next(accountInfo);
      });
    return accountSubject.asObservable();
  }

  async getAccountByPhone(
    phoneNumber: string,
    limit: number = 100
  ): Promise<Observable<AccountDetails>> {
    const accountSubject: Subject<any> = new Subject<any>();
    this.getAccountDetailsFromMeta(await Phone.toKey(phoneNumber))
      .pipe(first())
      .subscribe(async (res) => {
        const response: Syncable = Envelope.fromJSON(JSON.stringify(res)).unwrap();
        const address: string = response.m.data;
        const account: Observable<AccountDetails> = await this.getAccountByAddress(address, limit);
        account.subscribe((result) => {
          accountSubject.next(result);
        });
      });
    return accountSubject.asObservable();
  }

  resetAccountsList(): void {
    this.accounts = [];
    this.accountsList.next(this.accounts);
  }

  searchAccountByName(name: string): any {
    return;
  }

  getCategories(): Observable<any> {
    return this.httpClient.get(`${environment.cicMetaUrl}/categories`);
  }

  getCategoryByProduct(product: string): Observable<any> {
    return this.httpClient.get(`${environment.cicMetaUrl}/categories/${product.toLowerCase()}`);
  }

  getAccountTypes(): Observable<any> {
    return this.httpClient.get(`${environment.cicMetaUrl}/accounttypes`);
  }

  getTransactionTypes(): Observable<any> {
    return this.httpClient.get(`${environment.cicMetaUrl}/transactiontypes`);
  }

  getGenders(): Observable<any> {
    return this.httpClient.get(`${environment.cicMetaUrl}/genders`);
  }
}
