import type { Account, Address, SignPsbtParameters } from '@bigmi/core'
import {
  base64ToHex,
  getAddressChainId,
  getAddressInfo,
  hexToBase64,
  MethodNotSupportedRpcError,
  ProviderNotFoundError,
  UserRejectedRequestError,
  withTimeout,
} from '@bigmi/core'
import { ConnectorChainIdDetectionError } from '../errors/connectors.js'
import { createConnector } from '../factories/createConnector.js'
import type { CreateConnectorFn } from '../types/connector.js'
import type {
  ProviderRequestParams,
  UTXOConnectorParameters,
  UTXOWalletProvider,
} from './types.js'

type ReownConnectorProperties = {
  getAccounts(): Promise<readonly Account[]>
  onAccountsChanged(accounts: Address[]): void
  getInternalProvider(): Promise<InternalReownProvider | undefined>
} & UTXOWalletProvider

type InternalReownProvider = {
  id: string
  name: string
  imageUrl?: string
  connector: BitcoinConnector
  address: Address | undefined
}

export declare enum AddressPurpose {
  Ordinal = 'ordinal',
  Payment = 'payment',
  Stacks = 'stx',
}

type BitcoinConnector = {
  connect(): Promise<string>
  getAccountAddresses(): Promise<
    Array<{
      address: string
      publicKey?: string
      purpose: AddressPurpose
    }>
  >
  signPSBT(params: {
    psbt: string
    signInputs: Array<{
      address: string
      index: number
      sighashTypes: number[]
    }>
    broadcast?: boolean
  }): Promise<{ psbt: string; txid?: string }>
}

export type ReownWalletInfo = {
  name?: string
  icon?: string
}

export type ReownConnectorParameters = {
  connector: BitcoinConnector
  address?: Address
  walletInfo?: ReownWalletInfo
} & UTXOConnectorParameters

export function reown(
  parameters: ReownConnectorParameters
): CreateConnectorFn<UTXOWalletProvider | undefined, ReownConnectorProperties> {
  const {
    chainId,
    shimDisconnect = true,
    connector,
    address,
    walletInfo,
  } = parameters

  // Generate connector id and name from wallet info
  const id = walletInfo?.name?.toLowerCase().replace(/\s+/g, '-') || 'reown'
  const name = walletInfo?.name || 'Reown Bitcoin Wallet'
  const imageUrl = walletInfo?.icon

  return createConnector<
    UTXOWalletProvider | undefined,
    ReownConnectorProperties
  >((config) => ({
    id,
    name,
    type: reown.type,
    icon: imageUrl,
    async setup() {
      //
    },
    async getInternalProvider() {
      // Create internal provider that wraps the BitcoinConnector
      const internalProvider: InternalReownProvider = {
        id,
        name,
        imageUrl,
        connector,
        address,
      }
      return internalProvider
    },
    async getProvider() {
      const provider = await this.getInternalProvider()
      if (!provider) {
        return
      }
      const walletProvider = {
        request: this.request.bind(provider),
      }
      return walletProvider
    },
    async request(
      this: InternalReownProvider,
      { method, params }: ProviderRequestParams
    ): Promise<any> {
      switch (method) {
        case 'signPsbt': {
          const { psbt, ...options } = params as SignPsbtParameters
          const signInputs = options.inputsToSign.flatMap(
            ({ address, signingIndexes, sigHash }) =>
              signingIndexes.map((index) => ({
                address,
                index,
                sighashTypes: sigHash !== undefined ? [sigHash] : [],
              }))
          )

          const psbtBase64 = hexToBase64(psbt)

          const result = await this.connector.signPSBT({
            psbt: psbtBase64,
            signInputs,
            broadcast: false,
          })

          const signedPsbtHex = base64ToHex(result.psbt)
          return signedPsbtHex
        }
        default:
          throw new MethodNotSupportedRpcError(method)
      }
    },
    async connect() {
      const provider = await this.getInternalProvider()
      if (!provider) {
        throw new ProviderNotFoundError()
      }
      try {
        await provider.connector.connect()
        const accounts = await this.getAccounts()
        const detectedChainId = getAddressChainId(accounts[0].address)

        // Remove disconnected shim if it exists
        if (shimDisconnect) {
          await Promise.all([
            config.storage?.setItem(`${this.id}.connected`, true),
            config.storage?.removeItem(`${this.id}.disconnected`),
          ])
        }
        return { accounts, chainId: chainId ?? detectedChainId }
      } catch (error: any) {
        throw new UserRejectedRequestError(error.message)
      }
    },
    async disconnect() {
      // Add shim signalling connector is disconnected
      if (shimDisconnect) {
        await Promise.all([
          config.storage?.setItem(`${this.id}.disconnected`, true),
          config.storage?.removeItem(`${this.id}.connected`),
        ])
      }
    },
    async getAccounts() {
      const provider = await this.getInternalProvider()
      if (!provider) {
        throw new ProviderNotFoundError()
      }

      const createAccount = (addr: string, publicKey = ''): Account => {
        const { type, purpose } = getAddressInfo(addr)
        return { address: addr, addressType: type, publicKey, purpose }
      }

      try {
        const accounts = await withTimeout(
          () => provider.connector.getAccountAddresses(),
          { timeout: 1000 }
        )
        const paymentAccount = accounts.find((acc) => acc.purpose === 'payment')
        if (paymentAccount) {
          return [
            createAccount(
              paymentAccount.address,
              paymentAccount.publicKey ?? ''
            ),
          ]
        }
      } catch {
        // getAccountAddresses not supported or timed out
      }

      // fallback to provided address if available
      if (provider.address) {
        return [createAccount(provider.address)]
      }

      throw new ProviderNotFoundError()
    },
    async getChainId() {
      if (chainId) {
        return chainId
      }

      const accounts = await this.getAccounts()
      if (accounts.length === 0) {
        throw new ConnectorChainIdDetectionError({ connector: this.name })
      }
      return getAddressChainId(accounts[0].address)
    },
    async isAuthorized() {
      try {
        const isConnected =
          shimDisconnect &&
          // Check storage to see if a connection exists already
          Boolean(await config.storage?.getItem(`${this.id}.connected`))
        return isConnected
      } catch {
        return false
      }
    },
    async onAccountsChanged(accounts) {
      if (accounts.length === 0) {
        this.onDisconnect()
      } else {
        const newAccounts = await this.getAccounts()
        config.emitter.emit('change', {
          accounts: newAccounts,
        })
      }
    },
    onChainChanged(chainId) {
      config.emitter.emit('change', { chainId })
    },
    async onDisconnect(_error?) {
      // No need to remove `${this.id}.disconnected` from storage because `onDisconnect` is typically
      // only called when the wallet is disconnected through the wallet's interface, meaning the wallet
      // actually disconnected and we don't need to simulate it.
      config.emitter.emit('disconnect')
    },
  }))
}
export declare namespace reown {
  export var type: 'UTXO'
}
reown.type = 'UTXO' as const
