import * as Address from 'ox/Address'
import * as Hex from 'ox/Hex'
import * as P256 from 'ox/P256'
import * as PublicKey from 'ox/PublicKey'
import * as Secp256k1 from 'ox/Secp256k1'
import * as Signature from 'ox/Signature'
import { Channel, KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
import * as WebAuthnP256 from 'ox/WebAuthnP256'
import * as WebCryptoP256 from 'ox/WebCryptoP256'
import type {
  LocalAccount,
  Account as viem_Account,
} from '../accounts/types.js'
import { parseAccount } from '../accounts/utils/parseAccount.js'
import type { TransactionSerializable } from '../types/transaction.js'
import type { OneOf, RequiredBy } from '../types/utils.js'
import { hashAuthorization } from '../utils/authorization/hashAuthorization.js'
import { keccak256 } from '../utils/hash/keccak256.js'
import { hashMessage } from '../utils/signature/hashMessage.js'
import { hashTypedData } from '../utils/signature/hashTypedData.js'
import type { SerializeTransactionFn } from '../utils/transaction/serializeTransaction.js'
import * as Transaction from './Transaction.js'

export type Account_base<source extends string = string> = RequiredBy<
  LocalAccount<source>,
  'sign' | 'signAuthorization' | 'signTransaction'
> & {
  /** Key type. */
  keyType: SignatureEnvelope.Type
  /** Sign fn. */
  sign: NonNullable<LocalAccount['sign']>
  /** Sign transaction fn. */
  signTransaction: <
    serializer extends
      SerializeTransactionFn<TransactionSerializable> = SerializeTransactionFn<Transaction.TransactionSerializableTempo>,
    transaction extends Parameters<serializer>[0] = Parameters<serializer>[0],
  >(
    transaction: transaction,
    options?:
      | {
          serializer?: serializer | undefined
        }
      | undefined,
  ) => Promise<Hex.Hex>
  /** Sign voucher fn. */
  signVoucher: (
    parameters: signVoucher.Parameters,
  ) => Promise<signVoucher.ReturnValue>
}

export type RootAccount = Account_base<'root'> & {
  /** Sign key authorization. */
  signKeyAuthorization: (
    key: resolveAccessKey.Parameters,
    parameters: Pick<
      KeyAuthorization.KeyAuthorization,
      'chainId' | 'expiry' | 'limits' | 'scopes'
    >,
  ) => Promise<KeyAuthorization.Signed>
}

export type AccessKeyAccount = Account_base<'accessKey'> & {
  /** Access key ID. */
  accessKeyAddress: Address.Address
  /**
   * Signs a hash.
   *
   * By default, access key accounts sign through a keychain envelope so the
   * signature authorizes the parent account.
   *
   * Set `raw` to `true` to sign directly with the access key, without keychain
   * hashing or keychain enveloping.
   */
  sign: (parameters: {
    /** Hash to sign. */
    hash: Hex.Hex
    /** Sign directly with the access key, without keychain hashing or enveloping. */
    raw?: boolean | undefined
  }) => Promise<Hex.Hex>
}

export type Account = OneOf<RootAccount | AccessKeyAccount>

/** Instantiates an Account. */
export function from<const parameters extends from.Parameters>(
  parameters: parameters | from.Parameters,
): from.ReturnValue<parameters> {
  const { access } = parameters
  if (access) return fromAccessKey(parameters) as never
  return fromRoot(parameters) as never
}

export declare namespace from {
  export type Parameters = OneOf<fromRoot.Parameters | fromAccessKey.Parameters>

  export type ReturnValue<
    parameters extends {
      access?: fromAccessKey.Parameters['access'] | undefined
    } = {
      access?: fromAccessKey.Parameters['access'] | undefined
    },
  > = parameters extends {
    access: fromAccessKey.Parameters['access']
  }
    ? AccessKeyAccount
    : RootAccount
}

/**
 * Instantiates an Account from a headless WebAuthn credential (P256 private key).
 *
 * @example
 * ```ts
 * import { Account } from 'viem/tempo'
 *
 * const account = Account.fromHeadlessWebAuthn('0x...')
 * ```
 *
 * @param privateKey P256 private key.
 * @returns Account.
 */
export function fromHeadlessWebAuthn<
  const options extends fromHeadlessWebAuthn.Options,
>(
  privateKey: Hex.Hex,
  options: options | fromHeadlessWebAuthn.Options,
): fromHeadlessWebAuthn.ReturnValue<options> {
  const { access, rpId, origin, internal_version } = options

  const publicKey = P256.getPublicKey({ privateKey })

  return from({
    access,
    internal_version,
    keyType: 'webAuthn',
    publicKey,
    async sign({ hash }) {
      const { metadata, payload } = WebAuthnP256.getSignPayload({
        ...options,
        challenge: hash,
        rpId,
        origin,
      })
      const signature = P256.sign({
        payload,
        privateKey,
        hash: true,
      })
      return SignatureEnvelope.serialize({
        metadata,
        signature,
        publicKey,
        type: 'webAuthn',
      })
    },
  }) as never
}

export declare namespace fromHeadlessWebAuthn {
  export type Options = Omit<
    WebAuthnP256.getSignPayload.Options,
    'challenge' | 'rpId' | 'origin'
  > &
    Pick<from.Parameters, 'access' | 'internal_version'> & {
      rpId: string
      origin: string
    }

  export type ReturnValue<options extends Options = Options> =
    from.ReturnValue<options>
}

/**
 * Instantiates an Account from a P256 private key.
 *
 * @example
 * ```ts
 * import { Account } from 'viem/tempo'
 *
 * const account = Account.fromP256('0x...')
 * ```
 *
 * @param privateKey P256 private key.
 * @returns Account.
 */
export function fromP256<const options extends fromP256.Options>(
  privateKey: Hex.Hex,
  options: options | fromP256.Options = {},
): fromP256.ReturnValue<options> {
  const { access, internal_version } = options
  const publicKey = P256.getPublicKey({ privateKey })

  return from({
    access,
    internal_version,
    keyType: 'p256',
    publicKey,
    async sign({ hash }) {
      const signature = P256.sign({ payload: hash, privateKey })
      return SignatureEnvelope.serialize({
        signature,
        publicKey,
        type: 'p256',
      })
    },
  }) as never
}

export declare namespace fromP256 {
  export type Options = Pick<from.Parameters, 'access' | 'internal_version'>

  export type ReturnValue<options extends Options = Options> =
    from.ReturnValue<options>
}

/**
 * Instantiates an Account from a Secp256k1 private key.
 *
 * @example
 * ```ts
 * import { Account } from 'viem/tempo'
 *
 * const account = Account.fromSecp256k1('0x...')
 * ```
 *
 * @param privateKey Secp256k1 private key.
 * @returns Account.
 */
export function fromSecp256k1<const options extends fromSecp256k1.Options>(
  privateKey: Hex.Hex,
  options: options | fromSecp256k1.Options = {},
): fromSecp256k1.ReturnValue<options> {
  const { access, internal_version } = options
  const publicKey = Secp256k1.getPublicKey({ privateKey })

  return from({
    access,
    internal_version,
    keyType: 'secp256k1',
    publicKey,
    async sign(parameters) {
      const { hash } = parameters
      const signature = Secp256k1.sign({ payload: hash, privateKey })
      return Signature.toHex(signature)
    },
  }) as never
}

export declare namespace fromSecp256k1 {
  export type Options = Pick<from.Parameters, 'access' | 'internal_version'>

  export type ReturnValue<options extends Options = Options> =
    from.ReturnValue<options>
}

/**
 * Instantiates an Account from a WebAuthn credential.
 *
 * @example
 *
 * ### Create Passkey + Instantiate Account
 *
 * Create a credential with `WebAuthnP256.createCredential` and then instantiate
 * a Viem Account with `Account.fromWebAuthnP256`.
 *
 * It is highly recommended to store the credential's public key in an external store
 * for future use (ie. for future calls to `WebAuthnP256.getCredential`).
 *
 * ```ts
 * import { Account, WebAuthnP256 } from 'viem/tempo'
 * import { publicKeyStore } from './store'
 *
 * // 1. Create credential
 * const credential = await WebAuthnP256.createCredential({ name: 'Example' })
 *
 * // 2. Instantiate account
 * const account = Account.fromWebAuthnP256(credential)
 *
 * // 3. Store public key
 * await publicKeyStore.set(credential.id, credential.publicKey)
 *
 * ```
 *
 * @example
 *
 * ### Get Credential + Instantiate Account
 *
 * Gets a credential from `WebAuthnP256.getCredential` and then instantiates
 * an account with `Account.fromWebAuthnP256`.
 *
 * The `getPublicKey` function is required to fetch the public key paired with the credential
 * from an external store. The public key is required to derive the account's address.
 *
 * ```ts
 * import { Account, WebAuthnP256 } from 'viem/tempo'
 * import { publicKeyStore } from './store'
 *
 * // 1. Get credential
 * const credential = await WebAuthnP256.getCredential({
 *   async getPublicKey(credential) {
 *     // 2. Get public key from external store.
 *     return await publicKeyStore.get(credential.id)
 *   }
 * })
 *
 * // 3. Instantiate account
 * const account = Account.fromWebAuthnP256(credential)
 * ```
 *
 * @param credential WebAuthnP256 credential.
 * @returns Account.
 */
export function fromWebAuthnP256(
  credential: fromWebAuthnP256.Credential,
  options: fromWebAuthnP256.Options = {},
): fromWebAuthnP256.ReturnValue {
  const { id } = credential
  const publicKey = PublicKey.fromHex(credential.publicKey)
  return from({
    keyType: 'webAuthn',
    publicKey,
    async sign({ hash }) {
      const { metadata, signature } = await WebAuthnP256.sign({
        ...options,
        challenge: hash,
        credentialId: id,
      })
      return SignatureEnvelope.serialize({
        publicKey,
        metadata,
        signature,
        type: 'webAuthn',
      })
    },
  })
}

export declare namespace fromWebAuthnP256 {
  export type Credential = {
    id: WebAuthnP256.P256Credential['id']
    publicKey: Hex.Hex
  }

  export type Options = {
    getFn?: WebAuthnP256.sign.Options['getFn'] | undefined
    rpId?: WebAuthnP256.sign.Options['rpId'] | undefined
  }

  export type ReturnValue = from.ReturnValue
}

/**
 * Instantiates an Account from a P256 private key.
 *
 * @example
 * ```ts
 * import { Account } from 'viem/tempo'
 * import { WebCryptoP256 } from 'ox'
 *
 * const keyPair = await WebCryptoP256.createKeyPair()
 *
 * const account = Account.fromWebCryptoP256(keyPair)
 * ```
 *
 * @param keyPair WebCryptoP256 key pair.
 * @returns Account.
 */
export function fromWebCryptoP256<
  const options extends fromWebCryptoP256.Options,
>(
  keyPair: Awaited<ReturnType<typeof WebCryptoP256.createKeyPair>>,
  options: options | fromWebCryptoP256.Options = {},
): fromWebCryptoP256.ReturnValue<options> {
  const { access, internal_version } = options
  const { publicKey, privateKey } = keyPair

  return from({
    access,
    internal_version,
    keyType: 'p256',
    publicKey,
    async sign({ hash }) {
      const signature = await WebCryptoP256.sign({ payload: hash, privateKey })
      return SignatureEnvelope.serialize({
        signature,
        prehash: true,
        publicKey,
        type: 'p256',
      })
    },
  }) as never
}

export declare namespace fromWebCryptoP256 {
  export type Options = Pick<from.Parameters, 'access' | 'internal_version'>

  export type ReturnValue<options extends Options = Options> =
    from.ReturnValue<options>
}

export async function signVoucher(
  account: LocalAccount,
  parameters: signVoucher.Parameters,
): Promise<signVoucher.ReturnValue> {
  return account.sign!({
    hash: getVoucherSignPayload(parameters),
  })
}

function getVoucherSignPayload(parameters: signVoucher.Parameters) {
  const { chainId, channel, cumulativeAmount } = parameters
  const channelId =
    typeof channel === 'string'
      ? channel
      : Channel.computeId(channel, {
          chainId,
        })

  return Channel.getVoucherSignPayload({
    chainId,
    channelId,
    cumulativeAmount,
  })
}

export declare namespace signVoucher {
  type Parameters = {
    /** Chain ID. */
    chainId: number | bigint
    /** Channel descriptor or ID. */
    channel: Channel.computeId.Channel | Hex.Hex
    /** Total voucher amount signed for the channel. */
    cumulativeAmount: bigint
  }

  type ReturnValue = Hex.Hex
}

export async function signKeyAuthorization(
  account: LocalAccount,
  parameters: signKeyAuthorization.Parameters,
): Promise<signKeyAuthorization.ReturnValue> {
  const { chainId, key, expiry, limits, scopes } = parameters
  const { accessKeyAddress, keyType: type } = resolveAccessKey(key)

  const signature = await account.sign!({
    hash: KeyAuthorization.getSignPayload({
      address: accessKeyAddress,
      chainId,
      expiry,
      limits,
      scopes,
      type,
    }),
  })
  return KeyAuthorization.from({
    address: accessKeyAddress,
    chainId,
    expiry,
    limits,
    scopes,
    signature: SignatureEnvelope.from(signature),
    type,
  })
}

export declare namespace signKeyAuthorization {
  type Parameters = Pick<
    KeyAuthorization.KeyAuthorization,
    'chainId' | 'expiry' | 'limits' | 'scopes'
  > & {
    key: resolveAccessKey.Parameters
  }

  type ReturnValue = KeyAuthorization.Signed
}

/** @internal */
// biome-ignore lint/correctness/noUnusedVariables: _
function fromBase(parameters: fromBase.Parameters): Account_base {
  const {
    keyType = 'secp256k1',
    parentAddress,
    source = 'privateKey',
    internal_version = 'v2',
  } = parameters

  const address = parentAddress ?? Address.fromPublicKey(parameters.publicKey)
  const publicKey = PublicKey.toHex(parameters.publicKey, {
    includePrefix: false,
  })

  async function sign({ hash, raw }: { hash: Hex.Hex; raw?: boolean }) {
    if (raw) return await parameters.sign({ hash })
    const innerHash =
      parentAddress && internal_version === 'v2'
        ? keccak256(Hex.concat('0x04', hash, parentAddress))
        : hash
    const signature = await parameters.sign({ hash: innerHash })
    if (parentAddress)
      return SignatureEnvelope.serialize(
        SignatureEnvelope.from({
          userAddress: parentAddress,
          inner: SignatureEnvelope.from(signature),
          type: 'keychain',
          version: internal_version,
        }),
      )
    return signature
  }

  return {
    address: Address.checksum(address),
    keyType,
    sign,
    async signAuthorization(parameters) {
      const { chainId, nonce } = parameters
      const address = parameters.contractAddress ?? parameters.address
      const signature = await sign({
        hash: hashAuthorization({ address, chainId, nonce }),
      })
      const envelope = SignatureEnvelope.from(signature)
      if (envelope.type !== 'secp256k1')
        throw new Error(
          'Unsupported signature type. Expected `secp256k1` but got `' +
            envelope.type +
            '`.',
        )
      const { r, s, yParity } = envelope.signature
      return {
        address,
        chainId,
        nonce,
        r: Hex.fromNumber(r, { size: 32 }),
        s: Hex.fromNumber(s, { size: 32 }),
        yParity,
      }
    },
    async signMessage(parameters) {
      const { message } = parameters
      return await sign({ hash: hashMessage(message) })
    },
    async signTransaction(transaction, options) {
      const { serializer = Transaction.serialize } = options ?? {}
      const presign = (() => {
        if ('feePayerSignature' in transaction && transaction.feePayerSignature)
          return { ...transaction, feePayerSignature: null }
        return transaction
      })()
      const signature = await sign({
        hash: keccak256(await serializer(presign)),
      })
      const envelope = SignatureEnvelope.from(signature)
      return await serializer(transaction, envelope as never)
    },
    async signTypedData(typedData) {
      return await sign({ hash: hashTypedData(typedData) })
    },
    async signVoucher(parameters) {
      return await sign({
        hash: getVoucherSignPayload(parameters),
      })
    },
    publicKey,
    source,
    type: 'local',
  }
}

declare namespace fromBase {
  export type Parameters = {
    /** Parent address. */
    parentAddress?: Address.Address | undefined
    /** Public key. */
    publicKey: PublicKey.PublicKey
    /** Key type. */
    keyType?: SignatureEnvelope.Type | undefined
    /** Sign function. */
    sign: NonNullable<LocalAccount['sign']>
    /** Source. */
    source?: string | undefined
    /** Access key version. Will be removed in a future release. @deprecated @internal */
    internal_version?: 'v1' | 'v2' | undefined
  }

  export type ReturnValue = Account_base
}

/** @internal */
// biome-ignore lint/correctness/noUnusedVariables: _
function fromRoot(parameters: fromRoot.Parameters): RootAccount {
  const account = fromBase(parameters)
  return {
    ...account,
    source: 'root',
    async signKeyAuthorization(key, parameters) {
      const { chainId, expiry, limits, scopes } = parameters
      const { accessKeyAddress, keyType: type } = resolveAccessKey(key)

      const signature = await account.sign({
        hash: KeyAuthorization.getSignPayload({
          address: accessKeyAddress,
          chainId,
          expiry,
          limits,
          scopes,
          type,
        }),
      })
      const keyAuthorization = KeyAuthorization.from({
        address: accessKeyAddress,
        chainId,
        expiry,
        limits,
        scopes,
        signature: SignatureEnvelope.from(signature),
        type,
      })
      return keyAuthorization
    },
  }
}

declare namespace fromRoot {
  export type Parameters = fromBase.Parameters

  export type ReturnValue = RootAccount
}

// biome-ignore lint/correctness/noUnusedVariables: _
function fromAccessKey(parameters: fromAccessKey.Parameters): AccessKeyAccount {
  const { access } = parameters
  const { address: parentAddress } = parseAccount(access)
  const account = fromBase({ ...parameters, parentAddress })
  return {
    ...account,
    accessKeyAddress: Address.fromPublicKey(parameters.publicKey),
    source: 'accessKey',
  }
}

declare namespace fromAccessKey {
  export type Parameters = fromBase.Parameters & {
    /**
     * Parent account to access.
     * If defined, this account will act as an "access key", and use
     * the parent account's address as the keychain address.
     */
    access: viem_Account | Address.Address
  }

  export type ReturnValue = AccessKeyAccount
}

/** @internal */
export function resolveAccessKey(
  accessKey: resolveAccessKey.Parameters,
): resolveAccessKey.ReturnType {
  if ('accessKeyAddress' in accessKey)
    return {
      accessKeyAddress: accessKey.accessKeyAddress,
      keyType: accessKey.keyType,
    }
  if ('publicKey' in accessKey && accessKey.publicKey)
    return {
      accessKeyAddress: Address.fromPublicKey(
        PublicKey.fromHex(accessKey.publicKey),
      ),
      keyType: accessKey.type,
    }
  return {
    accessKeyAddress: accessKey.address,
    keyType: accessKey.type,
  }
}

export declare namespace resolveAccessKey {
  type Parameters =
    | Pick<AccessKeyAccount, 'accessKeyAddress' | 'keyType'>
    | OneOf<
        | {
            /** Access key address. */
            address: Address.Address
            /** Key type. */
            type: SignatureEnvelope.Type
          }
        | {
            /** Access key public key. */
            publicKey: Hex.Hex
            /** Key type. */
            type: SignatureEnvelope.Type
          }
      >

  type ReturnType = {
    accessKeyAddress: Address.Address
    keyType: SignatureEnvelope.Type
  }
}

// Export types required for inference.
// biome-ignore lint/performance/noBarrelFile: _
export {
  /** @deprecated */
  KeyAuthorization as z_KeyAuthorization,
  /** @deprecated */
  SignatureEnvelope as z_SignatureEnvelope,
  /** @deprecated */
  TxEnvelopeTempo as z_TxEnvelopeTempo,
} from 'ox/tempo'
