import { Hex, RpcResponse, Signature } from 'ox'
import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
import type { Address, Client } from 'viem'
import type { LocalAccount } from 'viem/accounts'
import { Transaction } from 'viem/tempo'

import * as Sponsorship from './sponsorship.js'
import * as Utils from './utils.js'

/** Default submission-claim TTL in milliseconds. */
export const defaultClaimTtl = 30_000

/** Returns whether a raw transaction carries a native multisig signature. */
export function isMultisigTransaction(serialized: Hex.Hex) {
  if (!Utils.isSerializedTempoTransaction(serialized)) return false
  try {
    const transaction = Transaction.deserialize(serialized as never)
    return isMultisigSignature((transaction as { signature?: unknown }).signature)
  } catch {
    return false
  }
}

/** Handles a native multisig raw transaction submission. */
export async function handleRawTransaction(
  options: handleRawTransaction.Options,
): Promise<handleRawTransaction.ReturnType> {
  const result = await collect(options)
  if (result.status.status === 'pending' || result.status.status === 'submitting')
    return result.status.operation.id
  if (result.status.status === 'submitted')
    return result.status.operation.submittedHash ?? result.status.operation.id
  if (options.method === 'eth_sendRawTransactionSync') return result.broadcastResult
  return result.status.operation.submittedHash!
}

export declare namespace handleRawTransaction {
  /** Options for handling a raw multisig transaction submission. */
  type Options = collect.Options

  /** Raw transaction handler return value. */
  type ReturnType = Hex.Hex | unknown
}

async function collect(options: collect.Options): Promise<collect.ReturnType> {
  const {
    claimTtl = defaultClaimTtl,
    finalize = 'sync',
    getClient,
    method,
    request,
    resolveConfig,
    sponsor,
    store,
  } = options
  const serialized = request.params?.[0]
  if (!Utils.isSerializedTempoTransaction(serialized))
    throw new RpcResponse.InvalidParamsError({
      message: 'Only Tempo (0x76/0x78) transactions are supported.',
    })

  const input = parse(serialized)
  const pending = await store.get(input.id)
  const config = await resolveValidatedConfig({
    account: input.account,
    chainId: input.chainId,
    init: input.init,
    pending,
    resolveConfig,
  })
  if (!config)
    throw new RpcResponse.InvalidParamsError({
      message:
        'Multisig config is required to collect approvals. Provide it in the bootstrap transaction or configure `multisig.resolveConfig`.',
    })
  const initConfig = resolveInitConfig({
    config,
    init: input.init,
    nonce: input.nonce,
    pending,
  })

  const now = Date.now()
  let operation: Operation = {
    account: input.account,
    chainId: input.chainId,
    claimExpiresAt: pending?.claimExpiresAt,
    config,
    createdAt: pending?.createdAt ?? now,
    id: input.id,
    init: pending?.init || !!initConfig,
    initConfig,
    payload: input.payload,
    signatures: mergeSignatures([...(pending?.signatures ?? []), ...input.signatures]),
    status: pending?.status === 'submitted' ? 'submitted' : 'pending',
    submittedHash: pending?.submittedHash,
    transaction: serialized as Hex.Hex,
    updatedAt: now,
  }

  let approvals = getApprovals({
    account: input.account,
    config,
    payload: input.payload,
    signatures: operation.signatures,
  })
  if (operation.status === 'submitted')
    return {
      broadcastResult: operation.submittedHash,
      status: { operation, status: 'submitted' },
    }

  if (approvals.weight < approvals.threshold) {
    operation = await store.savePending(operation)
    approvals = getApprovals({
      account: operation.account,
      config,
      payload: operation.payload,
      signatures: operation.signatures,
    })
    if (approvals.weight < approvals.threshold)
      return { status: { operation, status: operation.status } }
  }

  const claim = await store.claimSubmission(operation, { ttl: claimTtl })
  if (claim.status !== 'claimed') return { status: claim }

  operation = claim.operation
  approvals = getApprovals({
    account: operation.account,
    config,
    payload: operation.payload,
    signatures: operation.signatures,
  })
  const client = getClient(operation.chainId)
  const final = await serializeFinal({
    account: operation.account,
    init: operation.initConfig ?? initConfig,
    signatures: approvals.signatures,
    transaction: input.transaction,
  })
  const broadcastMethod =
    method === 'eth_sendRawTransaction' && finalize === 'sync'
      ? 'eth_sendRawTransactionSync'
      : method
  const feePayerState = (() => {
    try {
      const transaction = Transaction.deserialize(final as never) as Record<string, unknown>
      // No fee-payer field means finalized transaction does not request sponsorship.
      if (!('feePayerSignature' in transaction)) return {}
      // Preserve fee-token/signature marker for sponsorship decision below.
      return {
        feeToken:
          typeof transaction.feeToken === 'string' ? (transaction.feeToken as Address) : undefined,
        signature: transaction.feePayerSignature,
      }
    } catch {
      // Decode failure falls back to direct broadcast.
      return {}
    }
  })()
  const sponsorFeeToken = await (async () => {
    // No sponsor configured, so finalized multisig transaction broadcasts as-is.
    if (!sponsor) return undefined
    // Already fee-payer signed transactions must not be rewritten by this relay.
    if (feePayerState.signature !== null) return undefined
    // Explicit relay fee token wins when the transaction requests sponsorship.
    if (sponsor.feeToken) return sponsor.feeToken
    // `null` requests relay-added fee-payer signature.
    return await sponsor.resolveFeeToken?.(operation.chainId)
  })()
  const shouldSponsor = (() => {
    // No sponsor configured, so no sponsorship path to take.
    if (!sponsor) return false
    // `null` requests relay-added fee-payer signature.
    if (feePayerState.signature === null) return true
    // Missing or already-present fee-payer signatures broadcast unchanged.
    return false
  })()
  const result = await (async () => {
    // Sponsorship enabled, so sponsor adds/signs fee-payer fields.
    if (shouldSponsor && sponsor)
      return await Sponsorship.handleRawTransaction({
        account: sponsor.account,
        feeToken: sponsorFeeToken,
        getFeeToken: sponsor.resolveFeeToken
          ? async (chainId) => await sponsor.resolveFeeToken?.(chainId)
          : undefined,
        getClient,
        method: broadcastMethod,
        request: { params: [final] },
        sender: operation.account,
        validate: sponsor.validate,
      })
    // Otherwise, multisig transaction is final and can broadcast directly.
    return await client.request({
      method: broadcastMethod,
      params: [final],
    } as never)
  })()
  const submittedHash = (() => {
    if (typeof result === 'string') return result as Hex.Hex
    if (result && typeof result === 'object' && 'transactionHash' in result) {
      const response = result as { transactionHash?: unknown }
      if (typeof response.transactionHash === 'string') return response.transactionHash as Hex.Hex
    }
    throw new Error('Expected transaction hash in multisig broadcast result.')
  })()
  const submitted = await store.setSubmitted(operation.id, submittedHash)

  return {
    broadcastResult: result,
    status: { operation: submitted, status: 'claimed' },
  }
}

/** Resolves a multisig operation id for standard transaction lookup methods. */
export async function handleGetTransaction(
  options: handleGetTransaction.Options,
): Promise<handleGetTransaction.ReturnType> {
  const id = options.request.params?.[0]
  if (typeof id !== 'string') return undefined

  const operation = await options.store.get(id as Hex.Hex)
  if (!operation) return undefined
  if (operation.status !== 'submitted' || !operation.submittedHash) return { result: null }

  const result = await options.getClient(operation.chainId).request({
    method: options.method,
    params: [operation.submittedHash],
  } as never)
  return { result }
}

export declare namespace handleGetTransaction {
  /** Options for resolving a multisig operation id through transaction lookup methods. */
  type Options = {
    /** Client resolver keyed by transaction `chainId`. */
    getClient: (chainId?: number | undefined) => Client
    /** Standard transaction lookup method to handle. */
    method: 'eth_getTransactionByHash' | 'eth_getTransactionReceipt'
    /** Incoming JSON-RPC request. */
    request: { params?: readonly unknown[] | undefined }
    /** Pending multisig operation store. */
    store: Store
  }

  /** Transaction lookup result wrapper. */
  type ReturnType = { result: unknown } | undefined
}

declare namespace collect {
  type Options = {
    /** Submission claim TTL in milliseconds. @default 30000 */
    claimTtl?: number | undefined
    /** How to broadcast once quorum is met. @default 'sync' */
    finalize?: 'submitted' | 'sync' | undefined
    /** Client resolver keyed by transaction `chainId`. */
    getClient: (chainId?: number | undefined) => Client
    /** Raw transaction method to handle. */
    method: 'eth_sendRawTransaction' | 'eth_sendRawTransactionSync'
    /** Incoming JSON-RPC request. */
    request: { params?: readonly unknown[] | undefined }
    /** Resolves the genesis multisig config for quorum checks. */
    resolveConfig?: ResolveConfig | undefined
    /** Optional fee payer used to sponsor finalized multisig transactions. */
    sponsor?: Sponsor | undefined
    /** Pending multisig operation store. */
    store: Store
  }

  type ReturnType = {
    /** Raw broadcast result once quorum is met and this caller claimed submission. */
    broadcastResult?: unknown
    /** Structured operation claim status. */
    status: ClaimSubmissionResult | { operation: Operation; status: 'claimed' }
  }
}

/** Returns a stored native multisig operation status. */
export async function getStatus(options: getStatus.Options): Promise<Status | null> {
  const operation = await options.store.get(options.id)
  if (!operation) return null
  const config = await resolveValidatedConfig({
    account: operation.account,
    chainId: operation.chainId,
    pending: operation,
    resolveConfig: options.resolveConfig,
  })
  if (!config)
    return {
      account: operation.account,
      chainId: operation.chainId,
      id: operation.id,
      signatures: operation.signatures.length,
      status: operation.status,
      submittedHash: operation.submittedHash,
    }
  const approvals = getApprovals({
    account: operation.account,
    config,
    payload: operation.payload,
    signatures: operation.signatures,
  })
  return toStatus({ approvals, operation })
}

export declare namespace getStatus {
  /** Options for reading one multisig operation status. */
  type Options = {
    /** Operation id returned by a pending multisig submission. */
    id: Hex.Hex
    /** Resolves the genesis multisig config for quorum status. */
    resolveConfig?: ResolveConfig | undefined
    /** Pending multisig operation store. */
    store: Store
  }
}

/** Returns pending native multisig operation statuses for an account. */
export async function listStatuses(options: listStatuses.Options): Promise<readonly Status[]> {
  const operations = await options.store.listPendingByAddress(options.account)
  return await Promise.all(
    operations.map(async (operation) => {
      const config = await resolveValidatedConfig({
        account: operation.account,
        chainId: operation.chainId,
        pending: operation,
        resolveConfig: options.resolveConfig,
      })
      if (!config)
        return {
          account: operation.account,
          chainId: operation.chainId,
          id: operation.id,
          signatures: operation.signatures.length,
          status: operation.status,
          submittedHash: operation.submittedHash,
        }
      const approvals = getApprovals({
        account: operation.account,
        config,
        payload: operation.payload,
        signatures: operation.signatures,
      })
      return toStatus({ approvals, operation })
    }),
  )
}

export declare namespace listStatuses {
  /** Options for listing multisig operation statuses. */
  type Options = {
    /** Native multisig account address. */
    account: Address
    /** Resolves the genesis multisig config for quorum status. */
    resolveConfig?: ResolveConfig | undefined
    /** Pending multisig operation store. */
    store: Store
  }
}

/** Creates an in-memory multisig operation store for development and tests only. */
export function memoryStore(): Store {
  const operations = new Map<string, Operation>()
  return {
    async claimSubmission(operation, options) {
      const now = Date.now()
      const existing = operations.get(operation.id)
      if (existing?.status === 'submitted') return { operation: existing, status: 'submitted' }
      if (existing?.status === 'submitting' && (existing.claimExpiresAt ?? 0) > now)
        return { operation: existing, status: 'submitting' }
      const claimed = mergeOperation(existing, operation, {
        claimExpiresAt: now + options.ttl,
        status: 'submitting',
        updatedAt: now,
      })
      operations.set(operation.id, claimed)
      return { operation: claimed, status: 'claimed' }
    },
    async get(id) {
      return operations.get(id)
    },
    async listPendingByAddress(address) {
      return [...operations.values()].filter(
        (operation) =>
          operation.account.toLowerCase() === address.toLowerCase() &&
          operation.status !== 'submitted',
      )
    },
    async savePending(operation) {
      const now = Date.now()
      const existing = operations.get(operation.id)
      if (existing?.status === 'submitted') return existing
      const pending = mergeOperation(existing, operation, {
        status:
          existing?.status === 'submitting' && (existing.claimExpiresAt ?? 0) > now
            ? 'submitting'
            : 'pending',
        updatedAt: now,
      })
      operations.set(operation.id, pending)
      return pending
    },
    async setSubmitted(id, submittedHash) {
      const existing = operations.get(id)
      if (!existing) throw new Error('Cannot mark an unknown multisig operation submitted.')
      const submitted = {
        ...existing,
        claimExpiresAt: undefined,
        status: 'submitted',
        submittedHash,
        updatedAt: Date.now(),
      } satisfies Operation
      operations.set(id, submitted)
      return submitted
    },
  }
}

/** Native multisig relay options. */
export type Options = {
  /** Submission claim TTL in milliseconds. @default 30000 */
  claimTtl?: number | undefined
  /** How to broadcast once quorum is met. @default 'sync' */
  finalize?: 'submitted' | 'sync' | undefined
  /** Resolves the genesis multisig config for quorum checks. */
  resolveConfig?: ResolveConfig | undefined
  /** Pending multisig operation store. */
  store: Store
}

/** Resolves the genesis multisig config for quorum checks. */
export type ResolveConfig = (request: {
  /** Native multisig account address. */
  account: Address
  /** Transaction chain id. */
  chainId: number
  /** Bootstrap config carried by the transaction, when present. */
  init?: MultisigConfig.Config | undefined
}) => MultisigConfig.Config | Promise<MultisigConfig.Config | undefined> | undefined

/** Optional fee payer used to sponsor finalized multisig transactions. */
export type Sponsor = {
  /** Account used as the fee payer. */
  account: LocalAccount
  /** Fee token to set before fee payer signing. */
  feeToken?: Address | undefined
  /** Resolves the default fee token for raw sponsorship. */
  resolveFeeToken?:
    | ((chainId: number) => Address | Promise<Address | undefined> | undefined)
    | undefined
  /** Optional sponsorship approval callback. */
  validate?: Sponsorship.handleRawTransaction.Options['validate']
}

/** Storage for native multisig operation approvals and submission claims. */
export type Store = {
  /** Atomically claims submission for a quorum-ready operation. Only a `claimed` result may broadcast. */
  claimSubmission: (
    operation: Operation,
    options: claimSubmission.Options,
  ) => Promise<ClaimSubmissionResult>
  /** Reads a stored operation. */
  get: (id: Hex.Hex) => Promise<Operation | undefined>
  /** Lists non-submitted operations for a native multisig account address. */
  listPendingByAddress: (address: Address) => Promise<readonly Operation[]>
  /** Saves a pending operation, preserving already-stored approvals. */
  savePending: (operation: Operation) => Promise<Operation>
  /** Marks a claimed operation submitted with its real transaction hash. */
  setSubmitted: (id: Hex.Hex, submittedHash: Hex.Hex) => Promise<Operation>
}

/** `Store.claimSubmission` types. */
export declare namespace claimSubmission {
  /** Options for claiming submission. */
  type Options = {
    /** Claim TTL in milliseconds. */
    ttl: number
  }
}

/** Result of atomically claiming a multisig operation for submission. */
export type ClaimSubmissionResult =
  | {
      /** Operation this caller may broadcast. */
      operation: Operation
      /** Claim status. */
      status: 'claimed'
    }
  | {
      /** Current stored operation. */
      operation: Operation
      /** Current non-claimed status. */
      status: 'pending' | 'submitting' | 'submitted'
    }

/** Stored native multisig operation. */
export type Operation = {
  /** Native multisig account address. */
  account: Address
  /** Transaction chain id. */
  chainId: number
  /** Submission claim expiry timestamp in milliseconds. */
  claimExpiresAt?: number | undefined
  /** Resolved genesis config used for approval checks. */
  config?: MultisigConfig.Config | undefined
  /** Creation timestamp in milliseconds. */
  createdAt: number
  /** Deterministic operation id. */
  id: Hex.Hex
  /** Whether the finalized transaction must carry the genesis init config. */
  init?: boolean | undefined
  /** Genesis init config to attach when finalizing a bootstrap transaction. */
  initConfig?: MultisigConfig.Config | undefined
  /** Unsigned Tempo transaction sign payload. */
  payload: Hex.Hex
  /** Collected owner approval signatures. */
  signatures: readonly Hex.Hex[]
  /** Operation state. */
  status: 'pending' | 'submitting' | 'submitted'
  /** Transaction hash once submitted. */
  submittedHash?: Hex.Hex | undefined
  /** Last submitted serialized transaction carrying this operation. */
  transaction: Hex.Hex
  /** Last update timestamp in milliseconds. */
  updatedAt: number
}

/** Native multisig operation status. */
export type Status = {
  /** Native multisig account address. */
  account: Address
  /** Transaction chain id. */
  chainId: number
  /** Deterministic operation id. */
  id: Hex.Hex
  /** Number of collected owner approvals. */
  signatures: number
  /** Operation state. */
  status: Operation['status']
  /** Submitted transaction hash. */
  submittedHash?: Hex.Hex | undefined
  /** Required owner weight. */
  threshold?: number | undefined
  /** Collected owner weight. */
  weight?: number | undefined
}

function parse(serialized: Hex.Hex) {
  const transaction = Transaction.deserialize(serialized as never) as Record<string, unknown>
  const signature = transaction.signature
  if (!isMultisigSignature(signature))
    throw new RpcResponse.InvalidParamsError({
      message: 'Transaction does not contain a native multisig signature.',
    })

  const { signature: _, ...unsigned } = transaction
  const payload = TxEnvelopeTempo.getSignPayload(TxEnvelopeTempo.from(unsigned as never))
  const chainId = Number(transaction.chainId)
  const id = MultisigConfig.getSignPayload({
    account: signature.account,
    payload,
  })
  const init = signature.init ? MultisigConfig.from(signature.init) : undefined
  const nonce = typeof transaction.nonce === 'number' ? transaction.nonce : undefined

  return {
    account: signature.account,
    chainId,
    id,
    init,
    nonce,
    payload,
    signatures: signature.signatures.map((value) => SignatureEnvelope.serialize(value)),
    transaction,
  }
}

function isMultisigSignature(value: unknown): value is SignatureEnvelope.Multisig {
  return (
    typeof value === 'object' &&
    value !== null &&
    (value as { type?: unknown }).type === 'multisig' &&
    typeof (value as { account?: unknown }).account === 'string' &&
    Array.isArray((value as { signatures?: unknown }).signatures)
  )
}

async function resolveValidatedConfig(options: {
  account: Address
  chainId: number
  init?: MultisigConfig.Config | undefined
  pending?: Operation | undefined
  resolveConfig?: ResolveConfig | undefined
}) {
  const { account, chainId, init, pending, resolveConfig } = options
  const config = (await resolveConfig?.({ account, chainId, init })) ?? pending?.config ?? init
  if (!config) return undefined

  const normalized = MultisigConfig.from(config)
  // The multisig account address derives from the genesis config, so a
  // bootstrap init config must derive the transaction's account.
  if (
    init &&
    MultisigConfig.getAddress(MultisigConfig.from(init)).toLowerCase() !== account.toLowerCase()
  )
    throw new RpcResponse.InvalidParamsError({
      message: 'Bootstrap multisig init config does not match the multisig account.',
    })
  return normalized
}

function resolveInitConfig(options: {
  config: MultisigConfig.Config
  init?: MultisigConfig.Config | undefined
  nonce?: number | undefined
  pending?: Operation | undefined
}) {
  const { config, init, nonce, pending } = options
  if (pending?.initConfig) return pending.initConfig
  if (pending?.init && pending.config) return pending.config
  if (init) return init
  if (nonce === 0) return config
  return undefined
}

function mergeSignatures(signatures: readonly Hex.Hex[]) {
  const seen = new Set<string>()
  const out: Hex.Hex[] = []
  for (const signature of signatures) {
    const key = signature.toLowerCase()
    if (seen.has(key)) continue
    seen.add(key)
    out.push(signature)
  }
  return out
}

function getApprovals(options: {
  account: Address
  config: MultisigConfig.Config
  payload: Hex.Hex
  signatures: readonly Hex.Hex[]
}) {
  const { account, config, payload } = options
  const digest = MultisigConfig.getSignPayload({ account, payload })
  const owners = new Map(
    config.owners.map((owner) => [
      owner.owner.toLowerCase(),
      { address: owner.owner, weight: Number(owner.weight) },
    ]),
  )
  const seen = new Set<string>()
  const signatures: Hex.Hex[] = []
  let weight = 0

  for (const value of options.signatures) {
    const signature = SignatureEnvelope.from(value)
    const owner = SignatureEnvelope.extractAddress({ payload: digest, signature })
    const key = owner.toLowerCase()
    const configured = owners.get(key)
    if (!configured)
      throw new RpcResponse.InvalidParamsError({ message: `Signature from non-owner ${owner}.` })
    if (seen.has(key)) continue
    const valid = (() => {
      try {
        return SignatureEnvelope.verify(signature, { payload: digest, address: configured.address })
      } catch {
        return false
      }
    })()
    if (!valid)
      throw new RpcResponse.InvalidParamsError({
        message: `Invalid signature from owner ${configured.address}.`,
      })
    seen.add(key)
    signatures.push(SignatureEnvelope.serialize(signature))
    weight += configured.weight
  }

  return { signatures, threshold: Number(config.threshold), weight }
}

async function serializeFinal(options: {
  account: Address
  init?: MultisigConfig.Config | undefined
  signatures: readonly Hex.Hex[]
  transaction: Record<string, unknown>
}) {
  const { signature: _, ...transaction } = options.transaction
  const envelope = TxEnvelopeTempo.from({
    ...transaction,
    feePayerSignature: (() => {
      const value = transaction.feePayerSignature
      // Preserve missing/null fee-payer markers so sponsorship can add signature later.
      if (!value) return value
      // Normalize RPC-shaped signatures before rebuilding final transaction envelope.
      if (typeof value === 'object' && 'r' in value && 's' in value) {
        const signature = value as {
          r: bigint | number | string
          s: bigint | number | string
          v?: bigint | number | string | undefined
          yParity?: bigint | number | string | undefined
        }
        const yParity = (() => {
          const value = signature.yParity ?? signature.v
          // Some signature shapes omit parity until later normalization.
          if (typeof value === 'undefined') return undefined
          const number = Number(value)
          // RPC signatures may carry Ethereum 27/28 `v`; `ox` expects 0/1 parity.
          if (number === 27 || number === 28) return number - 27
          return number
        })()
        if (typeof yParity === 'number')
          return Signature.from({ r: BigInt(signature.r), s: BigInt(signature.s), yParity })
      }
      // Fall back to `ox` for already-normalized signature inputs.
      return Signature.from(value as never)
    })(),
  } as never)
  const payload = TxEnvelopeTempo.getSignPayload(envelope)
  const signatures = options.signatures.map((approval) => SignatureEnvelope.from(approval))
  const sorted = SignatureEnvelope.sortMultisigApprovals({
    account: options.account,
    payload,
    signatures,
  })
  const signature = SignatureEnvelope.from({
    account: options.account,
    signatures: sorted,
    ...(options.init ? { init: options.init } : {}),
  })
  return TxEnvelopeTempo.serialize(envelope, {
    ...('feePayerSignature' in transaction
      ? { feePayerSignature: transaction.feePayerSignature as never }
      : {}),
    signature,
  })
}

function mergeOperation(
  existing: Operation | undefined,
  operation: Operation,
  patch: Partial<Operation>,
) {
  return {
    ...operation,
    ...existing,
    ...patch,
    config: operation.config ?? existing?.config,
    init: existing?.init || operation.init,
    initConfig: existing?.initConfig ?? operation.initConfig,
    signatures: mergeSignatures([...(existing?.signatures ?? []), ...operation.signatures]),
    transaction: operation.transaction,
  } satisfies Operation
}

function toStatus(options: {
  approvals: { signatures: readonly Hex.Hex[]; threshold: number; weight: number }
  operation: Operation
}): Status {
  const { approvals, operation } = options
  return {
    account: operation.account,
    chainId: operation.chainId,
    id: operation.id,
    signatures: approvals.signatures.length,
    status: operation.status,
    submittedHash: operation.submittedHash,
    threshold: approvals.threshold,
    weight: approvals.weight,
  }
}
