import { ApiPromise, WsProvider } from '@polkadot/api';
import { SessionIndex, Registration, IdentityInfo, Event } from '@polkadot/types/interfaces';
import { Logger } from '@w3f/logger';
import { Text } from '@polkadot/types/primitive';
import {
    InputConfig, JudgementResult, WsChallengeRequest, WsChallengeUnrequest, WsPendingChallengesResponse, WsAck, WsDisplayNameResponse
} from '../types';
import { Option } from '@polkadot/types'
import fs from 'fs'
import { KeyringPair, KeyringPair$Json } from '@polkadot/keyring/types';
import {Keyring} from '@polkadot/keyring'
import { buildWsChallengeRequest, buildWsChallengeUnrequest, isJudgementGivenEvent, isJudgementUnrequested, isClaimChallengeCompliant, isJudgementRequestedEvent, isIdentityClearedEvent, extractJudgementInfoFromEvent, extractIdentityInfoFromEvent, buildWsChallengeRequestData, isIdentitySetEvent, buildJudgementGivenAck, extractRegistrationEntry, isDataPresent, isJudgementsFieldDisplayNamesCompliant, delay } from "../utils";
import { ISubscriber } from './ISubscriber'

export class Subscriber implements ISubscriber {
    private chain: Text;
    protected api: ApiPromise;
    private endpoint: string;
    private sessionIndex: SessionIndex;
    private logLevel: string;
    protected registrarIndex: number; 
    private registrarWalletFilePath: string;
    private registrarPasswordFilePath: string;
    protected registrarAccount: KeyringPair;
    private wsNewJudgementRequestHandler: (request: WsChallengeRequest) => void;
    private wsJudgementUnrequestedHandler: (message: WsChallengeUnrequest) => void;
    private wsJudgementGievenHandler: (message: WsAck) => void;

    constructor(
        cfg: InputConfig,
        protected readonly logger: Logger) {
        this.endpoint = cfg.node.endpoint;
        this.logLevel = cfg.logLevel;
        this.registrarIndex = cfg.registrar.index;
        this.registrarWalletFilePath = cfg.registrar.keystore.walletFilePath;
        this.registrarPasswordFilePath = cfg.registrar.keystore.passwordFilePath;
    }

    public start = async (): Promise<void> => {
        
        await this._initAPI();

        this._initKey()
        await this._initInstanceVariables();
        
        if(this.logLevel == 'debug') this._triggerDebugActions()

        await this._handleEventsSubscriptions();
    }

    private _initAPI = async (): Promise<void> =>{
      const endpoints = [this.endpoint]
      const provider = new WsProvider(endpoints);
      this.api = await ApiPromise.create({provider,throwOnConnect:true,throwOnUnknown:true})
      this.api.on('disconnected', async () => { 
        await delay(120000); //2 minute timeout
        if(!this.api.isConnected) 
          this.logger.error("Impossible to reconnect... exiting...")
          process.exit(-1);
      })
      
      this.chain = await this.api.rpc.system.chain();
      const [nodeName, nodeVersion] = await Promise.all([
          this.api.rpc.system.name(),
          this.api.rpc.system.version()
      ]);
      this.logger.info(
          `You are connected to chain ${this.chain} using ${nodeName} v${nodeVersion}`
      );
    }

    private _initKey = (): void =>{
      this.logger.debug(`init registrar with index ${this.registrarIndex} ...`)
      const keyring = new Keyring({ type: 'sr25519' });
      const keyJson = JSON.parse(fs.readFileSync(this.registrarWalletFilePath, { encoding: 'utf-8' })) as KeyringPair$Json;
      const passwordContent = fs.readFileSync(this.registrarPasswordFilePath, { encoding: 'utf-8' });
      this.registrarAccount = keyring.addFromJson(keyJson)
      this.registrarAccount.decodePkcs8(passwordContent)

      this.logger.debug(`read account with address: ${keyring.pairs[0].toJson().address}`)
      this.logger.debug(`is locked: ${this.registrarAccount.isLocked}`)

      if(this.registrarAccount.isLocked){
        this.logger.error(`problem unlocking the wallet, exiting ...`)
        process.exit(1)
      }
    }

    private _initInstanceVariables = async (): Promise<void> =>{
      this.sessionIndex = await this.api.query.session.currentIndex();
      this.logger.debug(
        `Session index: ${this.sessionIndex}`
      );
    }

    public setNewJudgementRequestHandler = (handler: (request: WsChallengeRequest) => void ): void => {
      this.wsNewJudgementRequestHandler = handler
    }

    public setJudgementUnrequestHandler = (handler: (request: WsChallengeUnrequest) => void ): void => {
      this.wsJudgementUnrequestedHandler = handler
    }

    public setJudgementGivenHandler = (handler: (request: WsAck) => void ): void => {
      this.wsJudgementGievenHandler = handler
    }

    private _triggerDebugActions = (): void =>{
      this.logger.debug('debug mode active')
    }
    
    private _handleEventsSubscriptions = async (): Promise<void> => {
      this.api.query.system.events((events) => {

        events.forEach(async (record) => {
          const { event } = record;

          await this._handleJudgementEvents(event)

          await this._handleIdentityEvents(event)

        })
      })
    }

    private _handleIdentityEvents = async (event: Event): Promise<void> => {

      if (isIdentityClearedEvent(event)) {
        this._identityClearedHandler(event)
      }

      if (isIdentitySetEvent(event)) {
        this._judgementUpdateHandler(event)
      }

    }

    private _handleJudgementEvents = async (event: Event): Promise<void> => {

      if (isJudgementRequestedEvent(event)) {
        await this._judgementRequestedHandler(event)
      }

      if (isJudgementGivenEvent(event)) {
        await this._judgementGivendHandler(event)
      }

      if (isJudgementUnrequested(event)) {
        await this._judgementUnrequestedHandler(event)
      }

    }

    private _identityClearedHandler = async (event: Event): Promise<void> => {
      this.logger.info('Identity Cleared Event Received')
      const accountId = extractIdentityInfoFromEvent(event)
      this.logger.info(`AccountId: ${accountId}`)

      try {
        this.wsJudgementUnrequestedHandler(buildWsChallengeUnrequest(accountId))
      } catch (error) {
        this.logger.error(`problem on notifying the challenger about the account ${accountId} JudgementUnrequested`)
        this.logger.error(error)
      }
      
    }

    private _hasIdentityAlreadyRequestedOurJudgement = async(accountId: string): Promise<boolean> => {

      let result = false
      
      const pending = await this.getAllOurPendingWsChallengeRequests()
      for(const data of pending.data){
        if(data.address == accountId) {
          result = true
          break
        }
      }

      return result
    }

    private _judgementUpdateHandler = async (event: Event): Promise<void> => {
      const accountId = extractIdentityInfoFromEvent(event)
      if( await this._hasIdentityAlreadyRequestedOurJudgement(accountId) ) {

        this.logger.info(`New Update Identity Event for ${accountId}`)
        this._performNewChallengeAttempt(accountId)
      }
    }

    private _judgementUnrequestedHandler = async (event: Event): Promise<void> => {
      this.logger.info('New JudgementUnrequested')
      const request = extractJudgementInfoFromEvent(event)
      this.logger.info('AccountId:'+request.accountId+'\tRegistrarIndex:'+request.registrarIndex)
      if(request.registrarIndex == this.registrarIndex) {
 
        try {
          this.wsJudgementUnrequestedHandler(buildWsChallengeUnrequest(request.accountId))
        } catch (error) {
          this.logger.error(`problem on notifying the challenger about a ${request.accountId} JudgementUnrequested`)
          this.logger.error(error)
        }

      }
    }

    private _judgementGivendHandler = async (event: Event): Promise<void> => {
      this.logger.info('New JudgementGiven')
      const request = extractJudgementInfoFromEvent(event)
      this.logger.info('AccountId:'+request.accountId+'\tRegistrarIndex:'+request.registrarIndex)
      // TODO should we do something particular if the judgement is provided by another requestor?
      if(request.registrarIndex == this.registrarIndex) {
        this.logger.info(`sending ack to challenger`)
        this.wsJudgementGievenHandler(buildJudgementGivenAck(request.accountId))
      }
    }

    private _judgementRequestedHandler = async (event: Event): Promise<void> => {
      this.logger.info('New JudgementRequested')
      const request = extractJudgementInfoFromEvent(event)
      this.logger.info('AccountId:'+request.accountId+'\tRegistrarIndex:'+request.registrarIndex)
      if(request.registrarIndex == this.registrarIndex) {
        this.logger.info(`event to be handled by registrar with index ${this.registrarIndex}`)
        this._performNewChallengeAttempt(request.accountId)       
      }
    }

    private _performNewChallengeAttempt = async (accountId: string): Promise<void> =>{
      const identity = await this._getIdentity(accountId)
      const judgements = identity.unwrapOrDefault().judgements
      const info: IdentityInfo = identity.unwrapOrDefault().info 
      this.logger.debug(info.toString())
      this.logger.debug(judgements.toString())
      this.logger.debug(identity.unwrapOrDefault().toString())

      if( identity.isEmpty ) {
        this.logger.info(`${accountId} has no active identity claims`)
        return
      }
      
      if( !isClaimChallengeCompliant(judgements, this.registrarIndex, info) ){
        this.logger.info(`${accountId} has a not interesting identity claim`)
        this.logger.info(`${judgements.toString()}`)
        this.logger.info(`${info.toString()}`)
        return
      }

      try {
        this.wsNewJudgementRequestHandler(buildWsChallengeRequest(accountId,info))
      } catch (error) {
        this.logger.error(`problem on performing a new challenge for account ${accountId}`)
        this.logger.error(error)
      }
    }    

    private _getIdentity = async (accountId: string): Promise<Option<Registration>> =>{
      return await this.api.query.identity.identityOf(accountId)
    }

    public handleTriggerExtrinsicJudgement = async (judgementResult: string, target: string): Promise<boolean> => {

      if( ! await this._isAccountIdWaitingOurJudgement(target) ){
        this.logger.info(`the account id ${target} is not present in the pending judgment request set for our registrar...`)
        return
      }
      
      this.logger.debug('Extrinsic to be handled with values...')
      this.logger.debug(judgementResult)
      this.logger.debug(target)

      try {

        if(judgementResult == JudgementResult[JudgementResult.erroneous] ){
          await this.triggerExtrinsicErroneous(target)
        }
        else if(judgementResult == JudgementResult[JudgementResult.reasonable] ){
          await this.triggerExtrinsicReasonable(target)
        }

        // the transaction has been successfully transmitted
        return true
        
      } catch (error) {
        this.logger.error(error)
        return false
      }
      
    }

    public triggerExtrinsicReasonable = async (target: string): Promise<void> => {
      await this._triggerExtrinsicProvideJudgement(target,{Reasonable: true})
    }

    public triggerExtrinsicErroneous = async (target: string): Promise<void> =>{
      await this._triggerExtrinsicProvideJudgement(target,{Erroneous: true})
    }

    protected _triggerExtrinsicProvideJudgement = async (target: string, judgement: {Reasonable: boolean} | {Erroneous: boolean} ): Promise<void> =>{      
      const extrinsic = this.api.tx.identity.provideJudgement(this.registrarIndex,target,judgement)
      const txHash = await extrinsic.signAndSend(this.registrarAccount)
      this.logger.info(`Judgement Submitted with hash ${txHash}`);
    }

    public getAllOurPendingWsChallengeRequests = async (): Promise<WsPendingChallengesResponse> => {

      const result: WsPendingChallengesResponse = {
        event: 'pendingJudgementsResponse',
        data: []
      }

      const entries = await this.api.query.identity.identityOf.entries()

      entries.forEach(([key, exposure]) => {

        const {accountId,judgements,info} = extractRegistrationEntry(key,exposure)

        this.logger.debug(`getAllOurPendingWsChallengeRequests candidates:`);
        this.logger.debug(`accountId: ${accountId}`);
        this.logger.debug(`\tregistration: ${judgements} `);
        this.logger.debug(`\tinfo: ${info} `);

        if(isClaimChallengeCompliant(judgements, this.registrarIndex, info)){
          result.data.push(buildWsChallengeRequestData(accountId, info))
        }

      })
    
      return result

    } 

    public getAllDisplayNames = async (): Promise<WsDisplayNameResponse> => {
      const result: WsDisplayNameResponse = {
        event: 'displayNamesResponse',
        data: []
      }

      const entries = await this.api.query.identity.identityOf.entries()

      entries.forEach(([key, exposure]) => {
        
        const {info, judgements, accountId} = extractRegistrationEntry(key,exposure)

        this.logger.debug(`getAllDisplayNames candidates`);
        this.logger.debug(`\tinfo: ${info}`);
        this.logger.debug(`\tjudgements: ${judgements}`);
        
        if(isJudgementsFieldDisplayNamesCompliant(judgements)){
          isDataPresent(info.display) && result.data.push({
            displayName:info.display.value.toHuman().toString(),
            address:accountId
          })
        }
      })

      return result
    }

    private _isAccountIdWaitingOurJudgement = async(acountId: string): Promise<boolean> => {
      let result = false

      const {data} = await this.getAllOurPendingWsChallengeRequests()
      for (const request of data) {
        if(request.address.trim() == acountId.trim()){
          result = true
          break
        }
      }

      return result
    }

}