"""
Client-side EIP-2612 permit + EIP-712 deposit authorization signing
for bilateral escrow agreements.

Agents use this to sign USDC permit approvals and deposit authorizations
WITHOUT sending their private keys to the server.

Two distinct EIP-712 domains:
  - USDC permit: chain-specific USDC name/version, verifyingContract=<USDC>
  - MeteringEscrow deposit auth: name="MeteringEscrow", version="1", verifyingContract=<escrow>

The SDK does NOT execute on-chain transactions. Claim/withdraw calldata
is returned unsigned. The agent signs and submits from their own wallet.
"""
from __future__ import annotations

import time
from typing import Any, Dict, Optional

from eth_account import Account
from eth_abi import encode as abi_encode
from eth_utils import keccak, to_checksum_address


# ========================================================================
# Chain Configuration — Pinned Contract Addresses
# ========================================================================

CHAIN_CONFIG: Dict[int, Dict[str, str]] = {
    # Base Sepolia (testnet)
    84532: {
        "usdc_address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        "escrow_contract_address": "0xcA98356B65951b621C32B840F0266847C6043B45",
        # Base Sepolia USDC advertises name() = "USDC" on-chain.
        "usdc_name": "USDC",
        "usdc_version": "2",
    },
    # Base mainnet
    8453: {
        "usdc_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "escrow_contract_address": "",  # TBD — pinned after deployment
        "usdc_name": "USD Coin",
        "usdc_version": "2",
    },
}

# Known function selectors for calldata verification
KNOWN_SELECTORS: Dict[str, str] = {
    "0x379607f5": "sellerClaim(uint256)",
    "0x2e1a7d4d": "buyerWithdraw(uint256)",
    # initiateClose(uint256)
    "0xc9b2e522": "initiateClose(uint256)",
}

# Deadline bounds (configurable)
MIN_DEADLINE_SECONDS = 60
MAX_DEADLINE_SECONDS = 3600


# ========================================================================
# Validation Helpers
# ========================================================================

def validate_chain_id(chain_id: int) -> Dict[str, str]:
    """Validate chain_id and return pinned config. Rejects unknown chains."""
    if chain_id not in CHAIN_CONFIG:
        raise ValueError(
            f"Unsupported chain_id: {chain_id}. "
            f"Supported: {list(CHAIN_CONFIG.keys())}"
        )
    return CHAIN_CONFIG[chain_id]


def validate_contract_address(chain_id: int, address: str, address_type: str) -> None:
    """Validate that a server-provided address matches pinned config."""
    config = validate_chain_id(chain_id)
    pinned = config.get(f"{address_type}_address", "")
    if not pinned:
        return  # Not yet pinned (escrow contract TBD)
    if address.lower() != pinned.lower():
        raise ValueError(
            f"Server-provided {address_type} address {address} does not match "
            f"pinned address {pinned} for chain {chain_id}"
        )


def validate_deadline(deadline: int, min_seconds: int = MIN_DEADLINE_SECONDS, max_seconds: int = MAX_DEADLINE_SECONDS) -> None:
    """Validate deadline is within acceptable bounds."""
    now = int(time.time())
    diff = deadline - now
    if diff < min_seconds:
        raise ValueError(
            f"Deadline too soon: {diff}s from now (minimum: {min_seconds}s)"
        )
    if diff > max_seconds:
        raise ValueError(
            f"Deadline too far: {diff}s from now (maximum: {max_seconds}s)"
        )


def validate_amount(
    deposit_params: Dict[str, Any],
    quantity: Optional[int] = None,
    price_per_unit_cents: Optional[int] = None,
) -> None:
    """Validate service_amount_atomic matches quantity * price if provided."""
    if quantity is None or price_per_unit_cents is None:
        return
    # Recompute: amount_cents = quantity * price_per_unit_cents
    # atomic = amount_cents * 10^4 (cents to 6-decimal USDC)
    expected_atomic = quantity * price_per_unit_cents * 10_000
    actual_atomic = deposit_params.get("service_amount_atomic", 0)
    if actual_atomic != expected_atomic:
        raise ValueError(
            f"Amount mismatch: server says {actual_atomic} atomic USDC, "
            f"but {quantity} * {price_per_unit_cents} cents = {expected_atomic} atomic USDC"
        )


def verify_calldata_selector(calldata_hex: str, escrow_contract: Optional[str] = None, to_address: Optional[str] = None) -> str:
    """Verify unsigned calldata has a known function selector.

    Args:
        calldata_hex: Hex-encoded calldata (with 0x prefix)
        escrow_contract: Expected contract address (optional, for to-address validation)
        to_address: The ``to`` field from the calldata response

    Returns:
        The function name if recognized

    Raises:
        ValueError: If selector is unknown or to-address doesn't match
    """
    if not calldata_hex or len(calldata_hex) < 10:
        raise ValueError("Calldata too short to contain a function selector")

    selector = calldata_hex[:10].lower()
    fn_name = KNOWN_SELECTORS.get(selector)
    if not fn_name:
        raise ValueError(
            f"Unknown function selector {selector}. "
            f"Known: {list(KNOWN_SELECTORS.values())}"
        )

    if escrow_contract and to_address:
        if to_address.lower() != escrow_contract.lower():
            raise ValueError(
                f"Calldata 'to' address {to_address} does not match "
                f"escrow contract {escrow_contract}"
            )

    return fn_name


# ========================================================================
# EIP-712 Type Hashes
# ========================================================================

# USDC EIP-2612 Permit typehash
PERMIT_TYPEHASH = keccak(
    text="Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
)

# MeteringEscrow deposit authorization typehash
DEPOSIT_TYPEHASH = keccak(
    text="Deposit(uint256 agreementId,uint256 deadline)"
)
CLOSE_INTENT_TYPEHASH = keccak(
    text="CloseIntent(uint256 agreementId,uint256 nonce,uint256 deadline)"
)
SELLER_CLAIM_INTENT_TYPEHASH = keccak(
    text="SellerClaimIntent(uint256 agreementId,uint256 nonce,uint256 deadline)"
)
BUYER_WITHDRAW_INTENT_TYPEHASH = keccak(
    text="BuyerWithdrawIntent(uint256 agreementId,uint256 nonce,uint256 deadline)"
)

EIP712_DOMAIN_TYPEHASH = keccak(
    text="EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
)


def _normalize_key(private_key: str) -> str:
    """Ensure private key has 0x prefix."""
    key = private_key.strip()
    if not key.startswith("0x"):
        key = "0x" + key
    return key


def _build_domain_separator(name: str, version: str, chain_id: int, contract_address: str) -> bytes:
    """Build EIP-712 domain separator."""
    return keccak(
        abi_encode(
            ["bytes32", "bytes32", "bytes32", "uint256", "address"],
            [
                EIP712_DOMAIN_TYPEHASH,
                keccak(text=name),
                keccak(text=version),
                chain_id,
                to_checksum_address(contract_address),
            ],
        )
    )


def _sign_eip712(digest: bytes, private_key: str) -> Dict[str, Any]:
    """Sign an EIP-712 digest and return signature components."""
    key = _normalize_key(private_key)
    signed = Account.unsafe_sign_hash(digest, private_key=key)
    return {
        "v": signed.v,
        "r": "0x" + signed.r.to_bytes(32, "big").hex() if isinstance(signed.r, int) else "0x" + signed.r.hex(),
        "s": "0x" + signed.s.to_bytes(32, "big").hex() if isinstance(signed.s, int) else "0x" + signed.s.hex(),
        "signature": "0x" + signed.signature.hex(),
    }


# ========================================================================
# Signing Functions
# ========================================================================

def sign_usdc_permit(
    *,
    buyer_private_key: str,
    spender: str,
    value: int,
    nonce: int,
    deadline: int,
    chain_id: int,
    usdc_address: Optional[str] = None,
) -> Dict[str, Any]:
    """Sign an EIP-2612 USDC permit allowing ``spender`` to transfer ``value``.

    Args:
        buyer_private_key: Agent's private key (stays local)
        spender: Address allowed to spend (escrow contract or gas wallet)
        value: Atomic USDC amount (6 decimals)
        nonce: USDC permit nonce for the buyer address
        deadline: Unix timestamp for permit expiry
        chain_id: Chain ID (84532 for Base Sepolia, 8453 for Base mainnet)
        usdc_address: Override USDC address (default: pinned per chain)

    Returns:
        Dict with ``signature``, ``v``, ``r``, ``s``, ``deadline``
    """
    config = validate_chain_id(chain_id)
    validate_deadline(deadline)

    usdc = usdc_address or config["usdc_address"]
    if usdc_address:
        validate_contract_address(chain_id, usdc_address, "usdc")

    key = _normalize_key(buyer_private_key)
    owner = Account.from_key(key).address

    # EIP-712 struct hash for Permit
    struct_hash = keccak(
        abi_encode(
            ["bytes32", "address", "address", "uint256", "uint256", "uint256"],
            [PERMIT_TYPEHASH, to_checksum_address(owner), to_checksum_address(spender), value, nonce, deadline],
        )
    )

    domain_separator = _build_domain_separator(
        config["usdc_name"], config["usdc_version"], chain_id, usdc
    )

    digest = keccak(b"\x19\x01" + domain_separator + struct_hash)

    result = _sign_eip712(digest, buyer_private_key)
    result["deadline"] = deadline
    return result


def sign_deposit_authorization(
    *,
    buyer_private_key: str,
    agreement_id: int,
    deadline: int,
    chain_id: int,
    escrow_contract_address: str,
) -> Dict[str, Any]:
    """Sign an EIP-712 deposit authorization for depositFor().

    Args:
        buyer_private_key: Agent's private key (stays local)
        agreement_id: On-chain agreement ID that depositFor() will verify
        deadline: Unix timestamp for authorization expiry
        chain_id: Chain ID
        escrow_contract_address: MeteringEscrow contract address

    Returns:
        Dict with ``signature``, ``deadline``
    """
    validate_chain_id(chain_id)
    validate_deadline(deadline)

    if escrow_contract_address:
        validate_contract_address(chain_id, escrow_contract_address, "escrow_contract")

    struct_hash = keccak(
        abi_encode(
            ["bytes32", "uint256", "uint256"],
            [DEPOSIT_TYPEHASH, agreement_id, deadline],
        )
    )

    domain_separator = _build_domain_separator(
        "MeteringEscrow", "1", chain_id, escrow_contract_address
    )

    digest = keccak(b"\x19\x01" + domain_separator + struct_hash)

    result = _sign_eip712(digest, buyer_private_key)
    result["deadline"] = deadline
    return result


def _sign_sponsored_action_intent(
    *,
    signer_private_key: str,
    agreement_id: int,
    nonce: int,
    deadline: int,
    chain_id: int,
    escrow_contract_address: str,
    typehash: bytes,
) -> Dict[str, Any]:
    """Sign an EIP-712 sponsored action authorization."""
    validate_chain_id(chain_id)
    validate_deadline(deadline)

    if escrow_contract_address:
        validate_contract_address(chain_id, escrow_contract_address, "escrow_contract")

    struct_hash = keccak(
        abi_encode(
            ["bytes32", "uint256", "uint256", "uint256"],
            [typehash, agreement_id, nonce, deadline],
        )
    )

    domain_separator = _build_domain_separator(
        "MeteringEscrow", "1", chain_id, escrow_contract_address
    )
    digest = keccak(b"\x19\x01" + domain_separator + struct_hash)

    result = _sign_eip712(digest, signer_private_key)
    result["nonce"] = nonce
    result["deadline"] = deadline
    return result


def sign_initiate_close_intent(
    *,
    signer_private_key: str,
    agreement_id: int,
    nonce: int,
    deadline: int,
    chain_id: int,
    escrow_contract_address: str,
) -> Dict[str, Any]:
    """Sign an EIP-712 authorization for initiateCloseWithSig()."""
    return _sign_sponsored_action_intent(
        signer_private_key=signer_private_key,
        agreement_id=agreement_id,
        nonce=nonce,
        deadline=deadline,
        chain_id=chain_id,
        escrow_contract_address=escrow_contract_address,
        typehash=CLOSE_INTENT_TYPEHASH,
    )


def sign_seller_claim_intent(
    *,
    signer_private_key: str,
    agreement_id: int,
    nonce: int,
    deadline: int,
    chain_id: int,
    escrow_contract_address: str,
) -> Dict[str, Any]:
    """Sign an EIP-712 authorization for sellerClaimWithSig()."""
    return _sign_sponsored_action_intent(
        signer_private_key=signer_private_key,
        agreement_id=agreement_id,
        nonce=nonce,
        deadline=deadline,
        chain_id=chain_id,
        escrow_contract_address=escrow_contract_address,
        typehash=SELLER_CLAIM_INTENT_TYPEHASH,
    )


def sign_buyer_withdraw_intent(
    *,
    signer_private_key: str,
    agreement_id: int,
    nonce: int,
    deadline: int,
    chain_id: int,
    escrow_contract_address: str,
) -> Dict[str, Any]:
    """Sign an EIP-712 authorization for buyerWithdrawWithSig()."""
    return _sign_sponsored_action_intent(
        signer_private_key=signer_private_key,
        agreement_id=agreement_id,
        nonce=nonce,
        deadline=deadline,
        chain_id=chain_id,
        escrow_contract_address=escrow_contract_address,
        typehash=BUYER_WITHDRAW_INTENT_TYPEHASH,
    )


def sign_infra_fee_permit(
    *,
    buyer_private_key: str,
    spender: str,
    value: int,
    nonce: int,
    deadline: int,
    chain_id: int,
    usdc_address: Optional[str] = None,
) -> Dict[str, Any]:
    """Sign an EIP-2612 USDC permit for the infrastructure fee.

    Same as ``sign_usdc_permit`` but ``spender`` is the gas wallet address
    (not the escrow contract).

    Args:
        buyer_private_key: Agent's private key (stays local)
        spender: Gas wallet address (fee recipient)
        value: Infra fee in atomic USDC
        nonce: USDC permit nonce (incremented from service permit nonce)
        deadline: Unix timestamp for permit expiry
        chain_id: Chain ID
        usdc_address: Override USDC address (default: pinned per chain)

    Returns:
        Dict with ``signature``, ``v``, ``r``, ``s``, ``deadline``
    """
    # Reuses sign_usdc_permit — same EIP-2612 domain, different spender
    return sign_usdc_permit(
        buyer_private_key=buyer_private_key,
        spender=spender,
        value=value,
        nonce=nonce,
        deadline=deadline,
        chain_id=chain_id,
        usdc_address=usdc_address,
    )


def build_bilateral_agreement_signatures(
    *,
    buyer_private_key: str,
    deposit_params: Dict[str, Any],
    quantity: int,
    price_per_unit_cents: int,
    permit_deadline: Optional[int] = None,
    deposit_auth_deadline: Optional[int] = None,
    usdc_permit_nonce: Optional[int] = None,
    infra_fee_permit_nonce: Optional[int] = None,
) -> Dict[str, Any]:
    """Build bilateral escrow signatures for proposal or funding refresh.

    Signs the escrow permit when ``usdc_permit_nonce`` is provided, signs the
    deposit authorization when ``deposit_params['onchain_agreement_id']`` is
    available, and optionally signs the infra fee permit. Returns a dict of
    fields that can be spread into ``UsageAgreementRequest`` or
    ``refresh_permit()``.

    Args:
        buyer_private_key: Agent's private key (stays local)
        deposit_params: Response from ``sdk.get_deposit_params()``
        quantity: Expected agreement quantity for amount validation
        price_per_unit_cents: Expected price in cents for amount validation
        permit_deadline: Unix timestamp for permit expiry (required when
            signing the initial escrow permit)
        deposit_auth_deadline: Unix timestamp for deposit auth expiry (required
            when signing deposit authorization)
        usdc_permit_nonce: Buyer's current USDC permit nonce (required when
            signing the initial escrow permit)
        infra_fee_permit_nonce: Nonce for infra fee permit (if infra fee applies)

    Returns:
        Dict with available signature fields:
            buyer_wallet_address, optionally buyer_permit_signature /
            buyer_permit_deadline, optionally buyer_deposit_auth_signature /
            buyer_deposit_auth_deadline, and optionally
            buyer_infra_fee_permit_signature / buyer_infra_fee_permit_deadline

    Example:
        >>> params = sdk.get_deposit_params(agreement_id)
        >>> sigs = build_bilateral_agreement_signatures(
        ...     buyer_private_key=os.environ["AGENT_PRIVATE_KEY"],
        ...     deposit_params=vars(params),  # or params.__dict__
        ...     quantity=100,
        ...     price_per_unit_cents=10,
        ...     permit_deadline=int(time.time()) + 600,
        ...     deposit_auth_deadline=int(time.time()) + 600,
        ...     usdc_permit_nonce=0,  # from USDC contract
        ... )
        >>> agreement = sdk.create_usage_agreement(UsageAgreementRequest(
        ...     seller_agent_id="seller-123",
        ...     quantity=100,
        ...     unit="api_calls",
        ...     price_per_unit_cents=10,
        ...     **sigs,
        ... ))
    """
    key = _normalize_key(buyer_private_key)
    buyer_address = Account.from_key(key).address

    chain_id = deposit_params["chain_id"]
    escrow_contract = deposit_params["escrow_contract_address"]
    service_amount = deposit_params["service_amount_atomic"]
    onchain_agreement_id = deposit_params.get("onchain_agreement_id")
    should_sign_permit = usdc_permit_nonce is not None
    should_sign_deposit_auth = onchain_agreement_id is not None

    if not should_sign_permit and not should_sign_deposit_auth:
        raise ValueError(
            "Either usdc_permit_nonce or deposit_params.onchain_agreement_id is required"
        )
    if should_sign_permit and permit_deadline is None:
        raise ValueError("permit_deadline is required when usdc_permit_nonce is provided")
    if should_sign_deposit_auth and deposit_auth_deadline is None:
        raise ValueError("deposit_auth_deadline is required when onchain_agreement_id is present")
    if infra_fee_permit_nonce is not None and permit_deadline is None:
        raise ValueError("permit_deadline is required when infra_fee_permit_nonce is provided")

    # Security hardening: validate server amount against expected agreement terms.
    validate_amount(
        deposit_params,
        quantity=quantity,
        price_per_unit_cents=price_per_unit_cents,
    )

    result: Dict[str, Any] = {
        "buyer_wallet_address": buyer_address,
    }

    if should_sign_permit:
        permit_result = sign_usdc_permit(
            buyer_private_key=buyer_private_key,
            spender=escrow_contract,
            value=service_amount,
            nonce=usdc_permit_nonce,
            deadline=permit_deadline,
            chain_id=chain_id,
            usdc_address=deposit_params.get("usdc_address"),
        )
        result["buyer_permit_signature"] = permit_result["signature"]
        result["buyer_permit_deadline"] = permit_deadline

    if should_sign_deposit_auth:
        deposit_auth_result = sign_deposit_authorization(
            buyer_private_key=buyer_private_key,
            agreement_id=int(onchain_agreement_id),
            deadline=deposit_auth_deadline,
            chain_id=chain_id,
            escrow_contract_address=escrow_contract,
        )
        result["buyer_deposit_auth_signature"] = deposit_auth_result["signature"]
        result["buyer_deposit_auth_deadline"] = deposit_auth_deadline

    # 3. Optionally sign infra fee permit
    infra_fee = deposit_params.get("infra_fee_amount_atomic")
    gas_wallet = deposit_params.get("gas_wallet_address")
    if infra_fee and gas_wallet and infra_fee_permit_nonce is not None:
        infra_result = sign_infra_fee_permit(
            buyer_private_key=buyer_private_key,
            spender=gas_wallet,
            value=infra_fee,
            nonce=infra_fee_permit_nonce,
            deadline=permit_deadline,
            chain_id=chain_id,
            usdc_address=deposit_params.get("usdc_address"),
        )
        result["buyer_infra_fee_permit_signature"] = infra_result["signature"]
        result["buyer_infra_fee_permit_deadline"] = permit_deadline

    return result
