import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { Injectable } from '@angular/core';
import { SignalRService } from '@shared/services/signalr-service';
import { WalletInfo } from '@shared/models/wallet-info';
import { Balances, TransactionsHistoryItem, WalletBalance, WalletHistory, WalletNamesData } from '@shared/services/interfaces/api.i';
import { SignalREvent, SignalREvents, WalletInfoSignalREvent } from '@shared/services/interfaces/signalr-events.i';
import { catchError, map, flatMap, tap } from 'rxjs/operators';
import { HttpClient, HttpParams } from '@angular/common/http';
import { RestApi } from '@shared/services/rest-api';
import { GlobalService } from '@shared/services/global.service';
import { ErrorService } from '@shared/services/error-service';
import { Transaction } from '@shared/models/transaction';
import { InterFluxTransaction } from '@shared/models/interflux-transaction';
import { TransactionSending } from '@shared/models/transaction-sending';
import { BuildTransactionResponse, InterFluxTransactionResponse, TransactionResponse } from '@shared/models/transaction-response';
import { FeeEstimation } from '@shared/models/fee-estimation';
import { WalletLoad } from '@shared/models/wallet-load';
import { WalletResync } from '@shared/models/wallet-rescan';
import { AddressBalance } from '@shared/models/address-balance';
import { SnackbarService } from 'ngx-snackbar';
import { NodeService } from '@shared/services/node-service';
import { TransactionInfo } from '@shared/models/transaction-info';
import { AddressBookService } from '@shared/services/address-book-service';
import { OpreturnTransaction } from '@shared/models/opreturn-transaction';
import { ExtPubKeyImport } from '@shared/models/extpubkey-import';
import { LoggerService } from '@shared/services/logger.service';

@Injectable({
  providedIn: 'root'
})
export class WalletService extends RestApi {
  public rescanInProgress: boolean;
  private transactionReceivedSubject = new Subject<SignalREvent>();
  private walletUpdatedSubjects: { [walletName: string]: BehaviorSubject<WalletBalance> } = {};
  private walletHistorySubjects: { [walletName: string]: BehaviorSubject<TransactionInfo[]> } = {};
  private loadingSubject = new Subject<boolean>();
  private walletActivitySubject = new Subject<boolean>();
  private currentWallet: WalletInfo;
  private historyPageSize = 40;
  public isSyncing: boolean;
  public ibdMode: boolean;

  public get loading(): Observable<boolean> {
    return this.loadingSubject.asObservable();
  }

  public get walletActivityFlag(): Observable<boolean> {
    return this.walletActivitySubject.asObservable();
  }

  public clearWalletActivityFlag(): void {
    this.walletActivitySubject.next(false);
  }

  constructor(
    private snackbarService: SnackbarService,
    private addressBookService: AddressBookService,
    private nodeService: NodeService,
    globalService: GlobalService,
    http: HttpClient,
    errorService: ErrorService,
    signalRService: SignalRService,
    loggerService: LoggerService) {
    super(globalService, http, errorService, loggerService);

    globalService.currentWallet.subscribe(wallet => {
      this.currentWallet = wallet;
    });

    // This covers sending and receiving as well as staking/mining events.
    signalRService.registerOnMessageEventHandler<SignalREvent>(SignalREvents.WalletProcessedTransactionOfInterestEvent,
    () => {
      this.refreshWallet();
    });

    this.nodeService.generalInfo().subscribe(generalInfo => {
      if (generalInfo.percentSynced === 100 && this.rescanInProgress) {
        this.rescanInProgress = false;
        this.snackbarService.add({
          msg: `Wallet rescan completed.`,
          customClass: 'notify-snack-bar',
          action: {
            text: null
          }
        });
      }
    });

    signalRService.registerOnMessageEventHandler<WalletInfoSignalREvent>(SignalREvents.WalletGeneralInfo,
                                                                         (message) => {
                                                                           // Update wallet history after chain is synced or IBD mode completed
                                                                           const syncCompleted = (this.isSyncing && message.lastBlockSyncedHeight === message.chainTip);
                                                                           let historyRefreshed = false;
                                                                           this.isSyncing = message.lastBlockSyncedHeight !== message.chainTip;
                                                                           this.ibdMode = !message.isChainSynced;

                                                                           if (syncCompleted) {
                                                                             historyRefreshed = true;
                                                                           }

                                                                           if (this.currentWallet && message.walletName === this.currentWallet.walletName) {
                                                                             const walletBalance = message.accountsBalances.find(acc => acc.accountName === this.currentWallet.account);
                                                                             this.updateWalletForCurrentAddress(walletBalance, historyRefreshed);
                                                                           }
                                                                         });
  }

  public getWalletNames(): Observable<WalletNamesData> {
    return this.get<WalletNamesData>('wallet/list-wallets').pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  public loadStratisWallet(data: WalletLoad): Observable<any> {
    return this.post('wallet/load/', data).pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  public removeWallet(walletName: string): Observable<any> {
    const params = new HttpParams()
      .set('walletName', walletName);

    return this.delete('wallet/remove-wallet', params).pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  public transactionReceived(): Observable<any> {
    return this.transactionReceivedSubject.asObservable();
  }

  public getAllAddressesForWallet(data: WalletInfo): Observable<AddressBalance> {
    return this.get<AddressBalance>('wallet/addresses', this.getWalletParams(data)).pipe(
      map((response => {
        response.addresses = response.addresses.sort((a, b) => b.amountConfirmed - a.amountConfirmed);
        return response;
      })),
      catchError(err => this.handleHttpError(err))
    );
  }

  public getUnusedReceiveAddress(data: WalletInfo): Observable<any> {
    return this.get('wallet/unusedaddress', this.getWalletParams(data)).pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  public rescanWallet(data: WalletResync): Observable<any> {
    const params = new HttpParams()
      .set('walletName', data.walletName)
      .set('all', data.all.toString())
      .set('reSync', data.reSync.toString());

    return this.delete('wallet/remove-transactions/', params).pipe(
      tap(() => {
        this.rescanInProgress = true;
        this.clearWalletHistory();
        //this.paginateHistory();
        this.getHistory();
      }),
      catchError(err => this.handleHttpError(err))
    );
  }

  public importExtPubKey(data: ExtPubKeyImport): Observable<any> {
    return this.post('wallet/recover-via-extpubkey', data).pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  public wallet(): Observable<WalletBalance> {
    return this.getWalletSubject().asObservable();
  }

  public walletHistory(): Observable<TransactionInfo[]> {
    return this.getWalletHistorySubject().asObservable();
  }

  public estimateFee(feeEstimation: FeeEstimation): Observable<any> {
    feeEstimation.shuffleOutputs = true;
    return this.post('wallet/estimate-txfee', feeEstimation).pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  public getHistory(): void {
    this.loadingSubject.next(true);
  
     let extra = Object.assign({}, {
      skip: 0,
      take: 1000
     }) as { [key: string]: any };

    this.get<WalletHistory>('wallet/history', this.getWalletParams(this.currentWallet, extra))
      .pipe(map((response) => {
        return response.history[0].transactionsHistory;
      }),
            catchError((err) => {
              this.loadingSubject.next(false);
              return this.handleHttpError(err);
            })).toPromise().then(history => {
        this.applyHistory(history);
        this.loadingSubject.next(false);
      });
  }

  // public paginateHistory(take?: number, prevOutputTxTime?: number, prevOutputIndex?: number): void {
  //   let extra = Object.assign({}, {
  //     prevOutputTxTime: prevOutputTxTime,
  //     prevOutputIndex: prevOutputIndex,
  //     take: take || this.historyPageSize
  //   }) as { [key: string]: any };

  //   this.loadingSubject.next(true);

  //   this.get<WalletHistory>('wallet/history', this.getWalletParams(this.currentWallet, extra))
  //     .pipe(map((response) => {
  //         return response.history[this.currentWallet.account].transactionsHistory;
  //       }),
  //       catchError((err) => {
  //         this.loadingSubject.next(false);
  //         return this.handleHttpError(err);
  //       })).toPromise().then(history => {
  //     this.applyHistory(history);
  //     this.loadingSubject.next(false);
  //   });
  // }

  private applyHistory(history: TransactionsHistoryItem[]): void {
    const subject = this.getWalletHistorySubject();
    const existingItems = subject.value;
    const newItems = [];
    history.forEach(item => {
      const index = existingItems.findIndex(existing => existing.id === item.id);
      if (index === -1) {
        const mapped = TransactionInfo.mapFromTransactionsHistoryItem(item, this.addressBookService);
        newItems.push(mapped);
      } else {
        if (item.confirmedInBlock && !existingItems[index].transactionConfirmedInBlock) {
          existingItems.filter(existing => existing.id === item.id).forEach(existing => {
            existing.transactionConfirmedInBlock = item.confirmedInBlock;
          });
        }
      }
    });
    const set = existingItems.concat(newItems);
    subject.next(set.sort((a, b) => b.timestamp - a.timestamp));
  }

  public broadcastTransaction(transactionHex: string): Observable<string> {
    return this.post('wallet/send-transaction', new TransactionSending(transactionHex)).pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  public sendTransaction(transaction: Transaction | OpreturnTransaction): Promise<TransactionResponse> {
      return this.buildAndSendTransaction(transaction).toPromise();
  }

  public sendInterFluxTransaction(transaction: InterFluxTransaction): Promise<InterFluxTransactionResponse> {    
      return this.buildAndSendInterFluxTransaction(transaction).toPromise();
  }

  public getTransactionCount(): Observable<number> {
    return this.get<any>('wallet/transactionCount', this.getWalletParams(this.currentWallet))
      .pipe(map(result => {
        return result.transactionCount as number;
      }), catchError(err => this.handleHttpError(err)));
  }

  private buildAndSendTransaction(transaction: Transaction | OpreturnTransaction): Observable<TransactionResponse> {
    const observable = this.post<BuildTransactionResponse>('wallet/build-transaction', transaction);

    return observable.pipe(
      map(response => {
        response.isSideChain = transaction.isSideChainTransaction;
        return response;
      }),
      flatMap((buildTransactionResponse) => {
        return this.post('wallet/send-transaction', new TransactionSending(buildTransactionResponse.hex)).pipe(
          map(() => {
            return new TransactionResponse(transaction, buildTransactionResponse.fee, buildTransactionResponse.isSideChain);
          }),
          tap(() => {
            this.refreshWallet();
          })
        );
      }),
      catchError(err => this.handleHttpError(err))
    );
  }

  private buildAndSendInterFluxTransaction(transaction: InterFluxTransaction): Observable<InterFluxTransactionResponse> {
    const observable = this.post<BuildTransactionResponse>('wallet/build-interflux-transaction', transaction);

    return observable.pipe(

      map(response => {
        response.isSideChain = transaction.isSideChainTransaction;
        return response;
      }),

      flatMap((buildTransactionResponse) => {
        return this.post('wallet/send-transaction', new TransactionSending(buildTransactionResponse.hex)).pipe(
          map(() => {
            return new InterFluxTransactionResponse(transaction, buildTransactionResponse.fee, buildTransactionResponse.isSideChain);
          }),
          tap(() => {
            this.refreshWallet();
          })
        );
      }),

      catchError(err => this.handleHttpError(err))
    );
  }

  private getWalletSubject(): BehaviorSubject<WalletBalance> {
    if (!this.walletUpdatedSubjects[this.currentWallet.walletName]) {
      this.walletUpdatedSubjects[this.currentWallet.walletName] = new BehaviorSubject<WalletBalance>(null);

      // Initialise the wallet
      this.getWalletBalance(this.currentWallet).toPromise().then(data => {
        if (data.balances.length > 0 && data.balances[this.currentWallet.account]) {
          this.updateWalletForCurrentAddress(data.balances[this.currentWallet.account]);
        }
      });
    }
    return this.walletUpdatedSubjects[this.currentWallet.walletName];
  }

  private getWalletHistorySubject(): BehaviorSubject<TransactionInfo[]> {
    if (!this.walletHistorySubjects[this.currentWallet.walletName, this.currentWallet.account]) {
      this.walletHistorySubjects[this.currentWallet.walletName, this.currentWallet.account] = new BehaviorSubject<TransactionInfo[]>([]);

      // Get initial Wallet History
      //this.paginateHistory(40);
      this.getHistory();
    }
    return this.walletHistorySubjects[this.currentWallet.walletName, this.currentWallet.account];
  }

  private getWalletBalance(data: WalletInfo): Observable<Balances> {
    return this.get<Balances>('wallet/balance', this.getWalletParams(data)).pipe(
      catchError(err => this.handleHttpError(err))
    );
  }

  private getWalletParams(walletInfo: WalletInfo, extra?: { [key: string]: any }): HttpParams {
    let params = new HttpParams()
      .set('walletName', walletInfo.walletName)
      .set('accountName', walletInfo.account || "account 0");

    if (extra) {
      Object.keys(extra).forEach(key => {
        if (extra[key] != null) {
          params = params.set(key, extra[key]);
        }
      });
    }
    return params;
  }

  private updateWalletForCurrentAddress(walletBalance?: WalletBalance, historyRefreshed?: boolean): void {
    if (!this.currentWallet) {
      return;
    }

    const walletSubject = this.getWalletSubject();
    const newBalance = new WalletBalance(
      walletBalance || walletSubject.value
    );

    if (!historyRefreshed && (walletSubject.value
      && (walletSubject.value.amountConfirmed !== newBalance.amountConfirmed
        || walletSubject.value.amountUnconfirmed !== newBalance.amountUnconfirmed))) {
      if (!this.rescanInProgress && !this.isSyncing) {
        this.walletActivitySubject.next(true);
      }
      //this.paginateHistory();
      this.getHistory();
    }

    walletSubject.next(newBalance);
  }

  private refreshWallet(): void {
    this.getWalletBalance(this.currentWallet).toPromise().then(
      wallet => {
        this.updateWalletForCurrentAddress(wallet.balances[this.currentWallet.account]);
      });
  }

  public clearWalletHistory(): void {
    if (this.currentWallet) {
      const walletHistorySubject = this.getWalletHistorySubject();
      walletHistorySubject.next([]);
    }
  }
}