import { types } from '@joystream/types'
import { ApiPromise, WsProvider } from '@polkadot/api'
import '@polkadot/api/augment'
import { Signer } from '@polkadot/api/types'
import { proxy } from 'comlink'

import { JoystreamLibError } from '@/joystream-lib/errors'
import { ConsoleLogger, SentryLogger } from '@/utils/logs'

import { JoystreamLibExtrinsics } from './extrinsics'
import { NFT_PERBILL_PERCENT } from './helpers'
import { AccountId } from './types'

export class JoystreamLib {
  readonly api: ApiPromise
  readonly extrinsics: JoystreamLibExtrinsics

  private _selectedAccountId: AccountId | null = null
  get selectedAccountId() {
    return this._selectedAccountId
  }

  // if needed these could become some kind of event emitter

  /* Lifecycle */
  constructor(endpoint: string, onNodeConnectionUpdate?: (connected: boolean) => unknown) {
    const provider = new WsProvider(endpoint)
    provider.on('connected', () => {
      this.logConnectionData(endpoint)
      onNodeConnectionUpdate?.(true)
    })
    provider.on('disconnected', () => {
      onNodeConnectionUpdate?.(false)
    })
    provider.on('error', () => {
      onNodeConnectionUpdate?.(false)
    })

    this.api = new ApiPromise({ provider, types })
    const extrinsics = new JoystreamLibExtrinsics(this.api, () => this.selectedAccountId, endpoint)
    this.extrinsics = proxy(extrinsics)
  }

  destroy() {
    this.api.disconnect()
    ConsoleLogger.log('[JoystreamLib] Destroyed')
  }

  private async ensureApi() {
    try {
      await this.api.isReady
    } catch (e) {
      SentryLogger.error('Failed to initialize Polkadot API', 'JoystreamLib', e)
      throw new JoystreamLibError({ name: 'ApiNotConnectedError' })
    }
  }

  private async logConnectionData(endpoint: string) {
    await this.ensureApi()
    const chain = await this.api.rpc.system.chain()
    ConsoleLogger.log(`[JoystreamLib] Connected to chain "${chain}" via "${endpoint}"`)
  }

  /* Public */
  async setActiveAccount(accountId: AccountId | null, signer?: Signer) {
    if (!accountId) {
      this._selectedAccountId = null
      this.api.setSigner({})
      return
    } else if (!signer) {
      SentryLogger.error('Missing signer for setActiveAccount', 'JoystreamLib')
      return
    }

    this._selectedAccountId = accountId
    this.api.setSigner(signer)
  }

  async getAccountBalance(accountId: AccountId): Promise<number> {
    await this.ensureApi()

    const { availableBalance } = await this.api.derive.balances.all(accountId)

    return availableBalance.toNumber()
  }

  async getCurrentBlock(): Promise<number> {
    await this.ensureApi()
    const header = await this.api.rpc.chain.getHeader()
    const { number } = header
    return number.toNumber()
  }

  async subscribeAccountBalance(accountId: AccountId, callback: (balance: number) => void) {
    await this.ensureApi()

    const unsubscribe = await this.api.derive.balances.all(accountId, ({ availableBalance }) => {
      callback(availableBalance.toNumber())
    })

    return proxy(unsubscribe)
  }

  async subscribeCurrentBlock(callback: (currentBlock: number) => void) {
    await this.ensureApi()
    const unsubscribe = await this.api.rpc.chain.subscribeNewHeads((result) => {
      const { number } = result
      callback(number.toNumber())
    })
    return proxy(unsubscribe)
  }

  async getNftChainState() {
    await this.ensureApi()

    const [
      maxAuctionDuration,
      minStartingPrice,
      auctionStartsAtMaxDelta,
      maxCreatorRoyalty,
      minCreatorRoyalty,
      platformFeePercentage,
    ] = await Promise.all([
      this.api.query.content.maxAuctionDuration(),
      this.api.query.content.minStartingPrice(),
      this.api.query.content.auctionStartsAtMaxDelta(),
      this.api.query.content.maxCreatorRoyalty(),
      this.api.query.content.minCreatorRoyalty(),
      this.api.query.content.platfromFeePercentage(),
    ])

    return {
      maxAuctionDuration: maxAuctionDuration.toNumber(),
      minStartingPrice: minStartingPrice.toNumber(),
      auctionStartsAtMaxDelta: auctionStartsAtMaxDelta.toNumber(),
      maxCreatorRoyalty: maxCreatorRoyalty.toNumber() / NFT_PERBILL_PERCENT,
      minCreatorRoyalty: minCreatorRoyalty.toNumber() / NFT_PERBILL_PERCENT,
      platformFeePercentage: platformFeePercentage.toNumber() / NFT_PERBILL_PERCENT,
    }
  }
}