"""Helpers and models for SwarmPay's on-chain escrow flow."""
from __future__ import annotations

import hashlib
from dataclasses import dataclass
from enum import Enum

from swarmpay.constants import (
    ESCROW_CONFIRM_DELIVERY_SELECTOR,
    ESCROW_DISPUTE_SELECTOR,
    ESCROW_FUND_SELECTOR,
    ESCROW_REFUND_SELECTOR,
    ESCROW_RELEASE_SELECTOR,
)
from swarmpay.wallet import AgentWallet


class OnchainEscrowStatus(str, Enum):
    """Mirror of the on-chain escrow state machine."""
    FUNDED = "funded"
    DELIVERED = "delivered"
    RELEASED = "released"
    REFUNDED = "refunded"
    DISPUTED = "disputed"


@dataclass(frozen=True)
class OnchainEscrowConfig:
    """Deployment-level settings for on-chain escrow."""
    chain_id: int
    contract_address: str
    token_address: str
    fee_recipient: str
    dispute_resolver: str
    confirmations_required: int = 1


@dataclass(frozen=True)
class OnchainEscrowIntent:
    """User intent before signing or broadcasting the fund transaction."""
    buyer_wallet: str
    provider_wallet: str
    token_address: str
    total_amount_atomic: int
    provider_amount_atomic: int
    fee_amount_atomic: int
    deadline_unix: int
    metadata_hash: str

    def validate(self) -> None:
        """Validate amounts and hash format before sending on-chain."""
        if self.total_amount_atomic <= 0:
            raise ValueError("Escrow total amount must be greater than 0")
        if self.provider_amount_atomic <= 0:
            raise ValueError("Provider amount must be greater than 0")
        if self.fee_amount_atomic < 0:
            raise ValueError("Fee amount cannot be negative")
        if self.provider_amount_atomic + self.fee_amount_atomic != self.total_amount_atomic:
            raise ValueError("Provider amount plus fee amount must equal total amount")
        if not self.metadata_hash.startswith("0x") or len(self.metadata_hash) != 66:
            raise ValueError("metadata_hash must be a 32-byte hex string")


@dataclass(frozen=True)
class OnchainEscrowRecord:
    """Canonical on-chain escrow representation used by Python callers."""
    escrow_id: str
    chain_id: int
    contract_address: str
    escrow_nonce: int
    buyer_wallet: str
    provider_wallet: str
    token_address: str
    total_amount_atomic: int
    provider_amount_atomic: int
    fee_amount_atomic: int
    deadline_unix: int
    status: OnchainEscrowStatus
    tx_hash: str = ""
    metadata_hash: str = ""


@dataclass(frozen=True)
class OnchainEscrowTx:
    """Raw transaction payload for an on-chain escrow action."""
    to: str
    data: str
    value: int = 0


@dataclass(frozen=True)
class OnchainEscrowFundingResult:
    """Result of approve + fund escrow transaction preparation/execution."""
    approval_tx_hash: str
    funding_tx_hash: str
    expected_escrow_id: str


@dataclass(frozen=True)
class OnchainEscrowFlowResult:
    """High-level result for a funded on-chain escrow flow."""
    escrow: OnchainEscrowRecord
    approval_tx_hash: str
    funding_tx_hash: str
    provider: dict | None = None


@dataclass(frozen=True)
class OnchainEscrowActionResult:
    """Result of an on-chain escrow action transaction."""
    escrow_id: str
    escrow_nonce: int
    status: OnchainEscrowStatus
    tx_hash: str
    reason_hash: str = ""


def _encode_uint256(value: int) -> str:
    if value < 0:
        raise ValueError("uint256 value cannot be negative")
    return hex(value)[2:].zfill(64)


def _encode_address(value: str) -> str:
    normalized = value.lower()
    if not normalized.startswith("0x") or len(normalized) != 42:
        raise ValueError("address must be a 20-byte hex string")
    return normalized[2:].zfill(64)


def _encode_bytes32(value: str) -> str:
    normalized = value.lower()
    if not normalized.startswith("0x") or len(normalized) != 66:
        raise ValueError("bytes32 value must be a 32-byte hex string")
    return normalized[2:]


def _encode_bool(value: bool) -> str:
    return _encode_uint256(1 if value else 0)


def _encode_call(selector: str, *args: str) -> str:
    return selector + "".join(args)


def build_escrow_id(chain_id: int, contract_address: str, escrow_nonce: int) -> str:
    """Build a stable escrow id that can be used across API/UI layers."""
    normalized = contract_address.lower()
    return f"escrow:{chain_id}:{normalized}:{escrow_nonce}"


def hash_escrow_metadata(service_type: str, reference: str = "", salt: str = "") -> str:
    """Hash public escrow metadata so descriptions need not live fully on-chain."""
    payload = "|".join([service_type.strip(), reference.strip(), salt.strip()])
    digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
    return f"0x{digest}"


def create_onchain_record(
    config: OnchainEscrowConfig,
    escrow_nonce: int,
    intent: OnchainEscrowIntent,
    status: OnchainEscrowStatus = OnchainEscrowStatus.FUNDED,
    tx_hash: str = "",
) -> OnchainEscrowRecord:
    """Create a Python-side record from deployment config plus a signed intent."""
    intent.validate()
    return OnchainEscrowRecord(
        escrow_id=build_escrow_id(config.chain_id, config.contract_address, escrow_nonce),
        chain_id=config.chain_id,
        contract_address=config.contract_address.lower(),
        escrow_nonce=escrow_nonce,
        buyer_wallet=intent.buyer_wallet,
        provider_wallet=intent.provider_wallet,
        token_address=intent.token_address,
        total_amount_atomic=intent.total_amount_atomic,
        provider_amount_atomic=intent.provider_amount_atomic,
        fee_amount_atomic=intent.fee_amount_atomic,
        deadline_unix=intent.deadline_unix,
        status=status,
        tx_hash=tx_hash,
        metadata_hash=intent.metadata_hash,
    )


class OnchainEscrowClient:
    """Minimal Python-side client for building and sending escrow contract calls."""

    def __init__(self, config: OnchainEscrowConfig):
        self.config = config

    def build_approve_tx(self, amount_atomic: int) -> OnchainEscrowTx:
        """Create the ERC-20 approve tx required before funding escrow."""
        if amount_atomic <= 0:
            raise ValueError("Approval amount must be greater than 0")
        data = "0x095ea7b3" + _encode_address(self.config.contract_address) + _encode_uint256(amount_atomic)
        return OnchainEscrowTx(to=self.config.token_address, data=data, value=0)

    def build_fund_tx(self, intent: OnchainEscrowIntent) -> OnchainEscrowTx:
        """Build calldata for fundEscrow()."""
        intent.validate()
        data = _encode_call(
            ESCROW_FUND_SELECTOR,
            _encode_address(intent.provider_wallet),
            _encode_address(intent.token_address),
            _encode_uint256(intent.total_amount_atomic),
            _encode_uint256(intent.provider_amount_atomic),
            _encode_uint256(intent.fee_amount_atomic),
            _encode_uint256(intent.deadline_unix),
            _encode_bytes32(intent.metadata_hash),
        )
        return OnchainEscrowTx(to=self.config.contract_address, data=data, value=0)

    def build_confirm_delivery_tx(self, escrow_nonce: int) -> OnchainEscrowTx:
        """Build calldata for confirmDelivery()."""
        return OnchainEscrowTx(
            to=self.config.contract_address,
            data=_encode_call(ESCROW_CONFIRM_DELIVERY_SELECTOR, _encode_uint256(escrow_nonce)),
            value=0,
        )

    def build_release_tx(self, escrow_nonce: int) -> OnchainEscrowTx:
        """Build calldata for releaseEscrow()."""
        return OnchainEscrowTx(
            to=self.config.contract_address,
            data=_encode_call(ESCROW_RELEASE_SELECTOR, _encode_uint256(escrow_nonce)),
            value=0,
        )

    def build_refund_tx(self, escrow_nonce: int) -> OnchainEscrowTx:
        """Build calldata for refundEscrow()."""
        return OnchainEscrowTx(
            to=self.config.contract_address,
            data=_encode_call(ESCROW_REFUND_SELECTOR, _encode_uint256(escrow_nonce)),
            value=0,
        )

    def build_dispute_tx(self, escrow_nonce: int, reason_hash: str) -> OnchainEscrowTx:
        """Build calldata for disputeEscrow()."""
        return OnchainEscrowTx(
            to=self.config.contract_address,
            data=_encode_call(
                ESCROW_DISPUTE_SELECTOR,
                _encode_uint256(escrow_nonce),
                _encode_bytes32(reason_hash),
            ),
            value=0,
        )

    def fund_with_wallet(
        self,
        wallet: AgentWallet,
        escrow_nonce: int,
        intent: OnchainEscrowIntent,
    ) -> OnchainEscrowFundingResult:
        """Approve token spend and fund escrow using the given wallet."""
        intent.validate()
        approval_tx = self.build_approve_tx(intent.total_amount_atomic)
        fund_tx = self.build_fund_tx(intent)
        approval_tx_hash = wallet.send_transaction(
            to=approval_tx.to,
            data=approval_tx.data,
            value=approval_tx.value,
        )
        funding_tx_hash = wallet.send_transaction(
            to=fund_tx.to,
            data=fund_tx.data,
            value=fund_tx.value,
        )
        expected_escrow_id = build_escrow_id(
            self.config.chain_id,
            self.config.contract_address,
            escrow_nonce,
        )
        return OnchainEscrowFundingResult(
            approval_tx_hash=str(approval_tx_hash),
            funding_tx_hash=str(funding_tx_hash),
            expected_escrow_id=expected_escrow_id,
        )

    def confirm_delivery_with_wallet(self, wallet: AgentWallet, escrow_nonce: int) -> str:
        tx = self.build_confirm_delivery_tx(escrow_nonce)
        return str(wallet.send_transaction(to=tx.to, data=tx.data, value=tx.value))

    def release_with_wallet(self, wallet: AgentWallet, escrow_nonce: int) -> str:
        tx = self.build_release_tx(escrow_nonce)
        return str(wallet.send_transaction(to=tx.to, data=tx.data, value=tx.value))

    def refund_with_wallet(self, wallet: AgentWallet, escrow_nonce: int) -> str:
        tx = self.build_refund_tx(escrow_nonce)
        return str(wallet.send_transaction(to=tx.to, data=tx.data, value=tx.value))

    def dispute_with_wallet(self, wallet: AgentWallet, escrow_nonce: int, reason_hash: str) -> str:
        tx = self.build_dispute_tx(escrow_nonce, reason_hash)
        return str(wallet.send_transaction(to=tx.to, data=tx.data, value=tx.value))
