// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0

import type { TezosCoinConfig } from './config'
import type { APIAccount } from './network/types'
import type { TezosOperationMode } from './types/model'
import { DerivationType } from '@taquito/ledger-signer'
import { compressPublicKey } from '@taquito/ledger-signer/dist/lib/utils'
import { validatePublicKey, ValidationResult, b58Encode, PrefixV2 } from '@taquito/utils'
import { hasManagerKey } from './network/types'

/**
 * Dust margin in mutez to prevent transaction failures on send max operations
 * This ensures a small safety buffer remains in the account after send max
 * Based on production behavior where ~281-287 mutez typically remain
 * Increased to 500 to handle edge cases and fee estimation variations
 */
export const DUST_MARGIN_MUTEZ = 500

// Tezos rejects `empty_implicit_delegated_contract`; stake-max leaves this buffer.
export const STAKE_USE_ALL_RESERVE_MUTEZ = 10_000n

/** Stake "Use Max": staked and unstaked-frozen funds count toward `balance` but aren't (re-)stakeable until finalized, so subtract both. */
export function computeMaxStakeAmount(
  balance: bigint,
  stakedBalance: bigint,
  unstakedBalance: bigint,
  fees: bigint
): bigint {
  const { spendable: liquid } = partitionNativeBalance(balance, stakedBalance, unstakedBalance)
  const reserved = fees + STAKE_USE_ALL_RESERVE_MUTEZ
  return liquid > reserved ? liquid - reserved : 0n
}

/**
 * Splits a Tezos native balance (which includes frozen funds) into spendable and locked
 * (staked + unstaked). `locked` is clamped to `total` so spendable stays >= 0 across TzKT reorgs.
 */
export function partitionNativeBalance(
  total: bigint,
  staked: bigint,
  unstaked: bigint
): { spendable: bigint; locked: bigint } {
  const frozen = staked + unstaked
  const locked = frozen > total ? total : frozen
  return { spendable: total - locked, locked }
}

/**
 * Suggested fee returned by Taquito for a minimal amount pre-estimation (mutez)
 * Used as a stable fallback for send-max when RPC estimation is unavailable.
 */
export const MIN_SUGGESTED_FEE_SMALL_TRANSFER = 489

/**
 * Typical operation size (bytes) for a simple XTZ transfer used to approximate
 * the increased fee component when computing send-max in fallback mode.
 * Chosen to match Taquito’s behavior closely in integration tests.
 */
export const OP_SIZE_XTZ_TRANSFER = 154

/** Minimal asset shape from `TransactionIntent` for FA2 detection */
export type TezosAssetLike = {
  type: string
  assetReference?: string
}

/**
 * Parse FA1.2 / FA2 token contract + token id from coin-framework `assetReference`
 * (`KT1…` or `KT1…:tokenId` as produced by getBlock / listOperations).
 */
export function parseTezosTokenAsset(
  asset: TezosAssetLike | undefined
): { contractAddress: string; tokenId: number } | null {
  if (!asset || asset.type === 'native') return null
  const ref = asset.assetReference?.trim()
  if (!ref?.startsWith('KT1')) return null

  const colonIdx = ref.lastIndexOf(':')
  if (colonIdx > 0) {
    const contractAddress = ref.slice(0, colonIdx)
    const tokenId = Number(ref.slice(colonIdx + 1))
    if (!Number.isFinite(tokenId) || tokenId < 0) return null
    return { contractAddress, tokenId }
  }

  return { contractAddress: ref, tokenId: 0 }
}

/**
 * Resolves Tezos operation mode from intent type and asset (native XTZ vs token).
 */
export function resolveTezosOperationMode(
  intentType: string,
  asset: TezosAssetLike | undefined
): TezosOperationMode {
  const base = intentType as TezosOperationMode
  if (base === 'send' && parseTezosTokenAsset(asset) !== null) {
    return 'send_token'
  }
  return base
}

/**
 * Creates a mock signer for Taquito operations (estimation and crafting)
 */
export function createMockSigner(publicKeyHash: string, publicKey: string) {
  return {
    publicKeyHash: async () => publicKeyHash,
    publicKey: async () => publicKey,
    sign: () => Promise.reject(new Error('unsupported')),
    secretKey: () => Promise.reject(new Error('unsupported')),
  }
}

/**
 * Normalize a Tezos public key to base58 format (edpk/sppk/p2pk) based on the
 * sender address prefix (tz1/tz2/tz3). Accepts either an already base58-encoded
 * key or a hex key returned by the Ledger app. Returns undefined if input is
 * empty or cannot be parsed.
 */
export function normalizePublicKeyForAddress(
  maybeKey: string | undefined,
  address: string
): string | undefined {
  if (!maybeKey) return undefined

  // If it's already a valid base58 public key, keep it
  if (validatePublicKey(maybeKey) === ValidationResult.VALID) return maybeKey

  // Attempt to convert hex → base58 according to address curve
  try {
    const pkHex = maybeKey.trim().toLowerCase()
    if (pkHex === '') return undefined
    const keyBuf = Buffer.from(pkHex, 'hex')

    // Choose curve/prefix from address tz1/tz2/tz3
    // default are values for tz1 and fallbacks to it
    let derivationType: DerivationType = DerivationType.ED25519
    let prefix: PrefixV2 = PrefixV2.Ed25519PublicKey

    if (address.startsWith('tz2')) {
      derivationType = DerivationType.SECP256K1
      prefix = PrefixV2.Secp256k1PublicKey
    } else if (address.startsWith('tz3')) {
      derivationType = DerivationType.P256
      prefix = PrefixV2.P256PublicKey
    }

    const RAW_ED25519_KEY_LENGTH = 32
    const COMPRESSED_SEC1_LENGTH = 33
    const UNCOMPRESSED_SEC1_LENGTH = 65
    const isEd25519 = derivationType === DerivationType.ED25519
    if (isEd25519) {
      if (keyBuf.length === COMPRESSED_SEC1_LENGTH) {
        return b58Encode(keyBuf.slice(1), prefix)
      }
      if (keyBuf.length !== RAW_ED25519_KEY_LENGTH) {
        return undefined
      }
      return b58Encode(keyBuf, prefix)
    }
    if (keyBuf.length === UNCOMPRESSED_SEC1_LENGTH) {
      return b58Encode(compressPublicKey(keyBuf, derivationType), prefix)
    }
    return b58Encode(keyBuf, prefix)
  } catch {
    return undefined
  }
}

/**
 * Converts a DER-encoded secp256k1/P-256 ECDSA signature (returned by the Tezos Ledger
 * app for tz2/tz3 accounts) to the raw 64-byte r||s format the Tezos protocol expects.
 * Format: 0x30 <totalLen> 0x02 <rLen> <r_bytes> 0x02 <sLen> <s_bytes>
 */
export function convertSecp256k1DERToRaw(derHex: string): string {
  if (!/^[0-9a-fA-F]+$/.test(derHex) || derHex.length % 2 !== 0) {
    throw new Error('Tezos: device returned an invalid signature')
  }
  const buf = Buffer.from(derHex, 'hex')
  if (buf.length === 64) return derHex // already raw r||s

  const rStart = 4
  const rLen = buf[3]
  const sLenIdx = rStart + rLen + 1
  const sLen = buf[sLenIdx]
  const sStart = sLenIdx + 1
  const malformed =
    buf.length < 8 ||
    buf[0] !== 0x30 ||
    buf[2] !== 0x02 ||
    buf[sLenIdx - 1] !== 0x02 ||
    rLen === 0 ||
    sLen === 0 ||
    rLen > 33 ||
    sLen > 33 ||
    sStart + sLen !== buf.length
  if (malformed) {
    throw new Error('Tezos: device returned an invalid signature')
  }

  const raw = Buffer.concat([
    normalizeTo32Bytes(buf.slice(rStart, rStart + rLen)),
    normalizeTo32Bytes(buf.slice(sStart, sStart + sLen)),
  ]).toString('hex')
  // A 33-byte r/s is only valid DER when left-padded with 0x00; anything else normalises
  // to a non-64-byte result and is not a usable signature.
  if (raw.length !== 128) {
    throw new Error('Tezos: device returned an invalid signature')
  }
  return raw
}

export function normalizeTo32Bytes(bytes: Buffer): Buffer {
  if (bytes.length === 33 && bytes[0] === 0x00) return bytes.slice(1)
  if (bytes.length < 32) {
    const padded = Buffer.alloc(32, 0)
    bytes.copy(padded, 32 - bytes.length)
    return padded
  }
  return bytes
}

/**
 * Creates default fallback estimation values
 */
export function createFallbackEstimation(config: TezosCoinConfig) {
  return {
    fees: BigInt(config.fees.minEstimatedFees),
    gasLimit: BigInt(config.fees.minGasLimit),
    storageLimit: BigInt(config.fees.minStorageLimit),
    estimatedFees: BigInt(config.fees.minEstimatedFees),
  }
}

/**
 * Whether an account has nothing on-chain that would spare a recipient the allocation
 * (storage) burn. Manager-key accounts (`user`/`delegate`) with a zero balance and truly
 * non-existent (`empty`) accounts count as empty. A registered baker (`delegate`) is always
 * allocated on-chain, so a zero-balance one is unusual — but keying on `hasManagerKey` keeps
 * the reveal/storage logic consistent for both wallet and baker recipients.
 */
export function hasEmptyBalance(account: APIAccount) {
  return (hasManagerKey(account) && account.balance === 0) || account.type === 'empty'
}
