/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
  BaseWalletAdapter,
  SendTransactionOptions,
  WalletAccountError,
  WalletDisconnectionError,
  WalletName,
  WalletNotReadyError,
  WalletPublicKeyError,
  WalletReadyState,
  WalletSignMessageError,
  WalletSignTransactionError,
  isVersionedTransaction,
  TransactionOrVersionedTransaction,
} from '@solana/wallet-adapter-base'
import {
  PublicKey,
  Keypair,
  type Connection,
  type Transaction,
  type VersionedTransaction,
  TransactionVersion,
} from '@solana/web3.js'
import EventEmitter from 'eventemitter3'
import nacl from 'tweetnacl'

interface E2EWallet extends EventEmitter {
  isE2E?: boolean;
  connect(): Promise<string>;
  disconnect(): Promise<void>;
  signTransaction<T extends Transaction | VersionedTransaction>(
    tx: T,
    publicKey?: PublicKey
  ): Promise<T>;
  sendTransactionWithSigners<T extends Transaction | VersionedTransaction>(
    transaction: T,
    connection: Connection,
    signers?: Keypair[]
  ): Promise<T>;
  signAllTransaction<T extends Transaction | VersionedTransaction>(
    txs: T[],
    publicKey?: PublicKey
  ): Promise<T[]>;
  signMessage(msg: Uint8Array, publicKey?: PublicKey): Promise<Uint8Array>;
  isConnected: boolean;
}

export interface E2EWalletAdapterConfig {
  keypair?: Keypair;
}

export const E2EWalletName = 'E2E' as WalletName<'E2E'>

export class E2EWalletAdapter extends BaseWalletAdapter {
  name = E2EWalletName
  url = 'http://2e2.renec.foundation/'
  icon = 'E2E'
  supportedTransactionVersions = new Set(['legacy', 0]) as ReadonlySet<TransactionVersion>

  private _connecting: boolean
  private _wallet: E2EWallet | null
  private _publicKey: PublicKey | null
  private _underlyingWallet: Keypair
  private _readyState: WalletReadyState = WalletReadyState.Installed

  constructor(config: E2EWalletAdapterConfig = {}) {
    super()
    this._connecting = false
    this._wallet = null
    this._publicKey = null
    this._underlyingWallet = config.keypair || Keypair.generate()
  }

  get publicKey() {
    return this._publicKey
  }

  get connecting() {
    return this._connecting
  }

  get connected() {
    return !!this._wallet?.isConnected
  }

  get readyState() {
    return this._readyState
  }

  get wallet() {
    return this._wallet
  }

  async connect(): Promise<void> {
    try {
      if (this.connected || this.connecting) return
      if (this._readyState !== WalletReadyState.Installed) throw new WalletNotReadyError()

      this._connecting = true

      if (this._underlyingWallet) {
        this._publicKey = this._underlyingWallet?.publicKey
      } else {
        throw new WalletAccountError('No account found')
      }

      this.emit('connect', this._publicKey)
    } catch (error: any) {
      this.emit('error', error)
      throw error
    } finally {
      this._connecting = false
    }
  }

  async disconnect(): Promise<void> {
    const wallet = this._wallet
    if (wallet) {
      this._wallet = null
      this._publicKey = null
      wallet.off('accountChanged', this._accountChanged)

      try {
        await wallet.disconnect()
      } catch (error: any) {
        this.emit('error', new WalletDisconnectionError(error?.message, error))
      }
    }

    this.emit('disconnect')
  }

  async sendTransaction(
    transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>,
    connection: Connection,
    options: SendTransactionOptions,
    signers?: Keypair[],
  ) {
    const allSigners = [...(signers || []), this._underlyingWallet]
    if (isVersionedTransaction(transaction)) {
      transaction.sign(allSigners)
      return await connection.sendTransaction(transaction, { maxRetries: 5 })
    } else {
      return await connection.sendTransaction(transaction, allSigners)
    }
  }

  async sendTransactionWithSigners<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
    transaction: T,
    connection: Connection,
    signers?: Keypair[],
  ): Promise<string> {
    try {
      return await this.sendTransaction(transaction, connection, {}, signers)
    } catch (error: any) {
      this.emit('error', new WalletSignTransactionError(error?.message, error))
      throw error
    }
  }

  signTransaction(transaction: TransactionOrVersionedTransaction<this['supportedTransactionVersions']>) {
    if (window.Cypress.env('E2E_WALLET_DISABLE_TRANSACTION_SIGNING')) {
      this.emit(
        'error',
        new WalletSignTransactionError('Cypress has requested to decline all transaction signing.'),
      )
    } else {
      if (isVersionedTransaction(transaction)) {
        transaction.sign([this._underlyingWallet])
      } else {
        transaction.partialSign(this._underlyingWallet)
      }
    }

    return transaction
  }

  async signAllTransactions<T extends TransactionOrVersionedTransaction<this['supportedTransactionVersions']>>(
    transactions: T[],
  ): Promise<T[]> {
    try {
      for (const tx of transactions) {
        this.signTransaction(tx)
      }
      return transactions
    } catch (error: any) {
      this.emit('error', new WalletSignTransactionError(error?.message, error))
      throw error
    }
  }

  async signMessage(message: Uint8Array): Promise<Uint8Array> {
    try {
      if (window.Cypress.env('E2E_WALLET_DISABLE_MESSAGE_SIGNING')) {
        throw new WalletSignMessageError('Cypress has requested to decline all message signing.')
      }
      return nacl.sign.detached(message, this._underlyingWallet?.secretKey)
    } catch (error: any) {
      this.emit('error', new WalletSignMessageError(error?.message, error))
      throw error
    }
  }

  private _accountChanged = (newPublicKeyStr?: string) => {
    if (!newPublicKeyStr) return

    const publicKey = this._publicKey
    if (!publicKey) return

    let newPublicKey
    try {
      newPublicKey = new PublicKey(newPublicKeyStr)
    } catch (error: any) {
      this.emit('error', new WalletPublicKeyError(error?.message, error))
      return
    }

    if (!newPublicKey || publicKey.equals(newPublicKey)) return

    this._publicKey = newPublicKey
    this.emit('connect', newPublicKey)
  }
}
