"""
patly/client.py

Your private key is used ONLY to:
  1. Sign the redeemPositions() transaction calldata (local, cryptographic)
  2. Sign the $0.01 USDC payment authorization (local, cryptographic)

In both cases only the SIGNATURE is transmitted — never the key itself.
This is identical to how MetaMask works. You can verify this with any
network packet inspector (e.g. Wireshark, Charles Proxy).

Usage:
    from patly import Patly

    p = Patly(api_key="your_key", pk="your_private_key")
    p.redeem("btc-updown-15m-1234567890", "YES")
"""

import base64
import json
import logging
import os
import secrets
import time
from typing import Optional

import requests
from eth_account import Account
from web3 import Web3

log = logging.getLogger("patly")

_DEFAULT_URL = "http://patly.duckdns.org"
_POLYGON_RPC = "https://polygon.drpc.org/"

_USDC        = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
_CTF         = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"

_USDC_DOMAIN = {
    "name": "USD Coin", "version": "2",
    "chainId": 137,
    "verifyingContract": _USDC,
}
_TRANSFER_TYPE = {
    "TransferWithAuthorization": [
        {"name": "from",        "type": "address"},
        {"name": "to",          "type": "address"},
        {"name": "value",       "type": "uint256"},
        {"name": "validAfter",  "type": "uint256"},
        {"name": "validBefore", "type": "uint256"},
        {"name": "nonce",       "type": "bytes32"},
    ]
}

_CTF_ABI = [{
    "inputs": [
        {"name": "collateralToken",    "type": "address"},
        {"name": "parentCollectionId", "type": "bytes32"},
        {"name": "conditionId",        "type": "bytes32"},
        {"name": "indexSets",          "type": "uint256[]"},
    ],
    "name": "redeemPositions", "outputs": [],
    "stateMutability": "nonpayable", "type": "function",
}, {
    "inputs":  [{"name": "conditionId", "type": "bytes32"}],
    "name":    "payoutDenominator",
    "outputs": [{"name": "", "type": "uint256"}],
    "stateMutability": "view", "type": "function",
}]


class Patly:
    """
    Patly — gasless Polymarket position redemption.

    Your private key (pk) is used only for local cryptographic signing.
    It is never transmitted, logged, or stored outside your machine.

    Args:
        api_key: Your Patly API key (from patly.duckdns.org/register)
        pk:      Private key of your Polymarket wallet
        url:     Patly service URL (default: https://patly.duckdns.org)
    """

    def __init__(self, api_key: str, pk: str, url: str = _DEFAULT_URL):
        self._api_key  = api_key
        self._account  = Account.from_key(pk)
        self._wallet   = self._account.address
        self._url      = url.rstrip("/")
        self._headers  = {"x-api-key": api_key, "Content-Type": "application/json"}
        self._w3       = Web3(Web3.HTTPProvider(_POLYGON_RPC))

        # Lazy-load SDK components
        self._signer   = None
        self._config   = None
        self._safe_fn  = None
        self._derive   = None
        self._build_fn = None

    def _init_sdk(self):
        """Lazy-load the Safe signing SDK."""
        if self._signer is not None:
            return
        from py_builder_relayer_client.signer import Signer
        from py_builder_relayer_client.config import get_contract_config
        from py_builder_relayer_client.builder.safe import build_safe_transaction_request
        from py_builder_relayer_client.builder.derive import derive
        from py_builder_relayer_client.models import (
            SafeTransaction, SafeTransactionArgs, OperationType
        )
        self._signer      = Signer(self._account.key.hex(), 137)
        self._config      = get_contract_config(137)
        self._build_fn    = build_safe_transaction_request
        self._derive      = derive
        self._SafeTx      = SafeTransaction
        self._SafeArgs    = SafeTransactionArgs
        self._OpType      = OperationType

    def _get_nonce(self) -> int:
        """Fetch current Safe nonce from Patly service."""
        r = requests.get(
            f"{self._url}/nonce",
            params={"wallet": self._wallet},
            headers=self._headers,
            timeout=10,
        )
        if not r.ok:
            raise RuntimeError(f"Could not get nonce: {r.text}")
        return r.json().get("nonce", 0)

    def _fetch_condition_id(self, slug: str) -> Optional[str]:
        """Fetch conditionId for a market slug."""
        r = requests.get(
            f"{self._url}/condition",
            params={"slug": slug},
            headers=self._headers,
            timeout=10,
        )
        if not r.ok:
            return None
        return r.json().get("condition_id")

    def _is_resolved(self, condition_id: str) -> bool:
        """Check on-chain if market is resolved."""
        try:
            ctf = self._w3.eth.contract(
                address=Web3.to_checksum_address(_CTF), abi=_CTF_ABI
            )
            cid = bytes.fromhex(condition_id.replace("0x", ""))
            return ctf.functions.payoutDenominator(cid).call() > 0
        except Exception:
            return False

    def _build_signed_tx(self, condition_id: str, won_side: str, slug: str = "") -> dict:
        """
        Build and sign a redeemPositions() Safe transaction.
        Signing happens entirely on this machine using the provided PK.
        Only the resulting signature is included in the output — not the PK.
        """
        self._init_sdk()

        ctf = self._w3.eth.contract(
            address=Web3.to_checksum_address(_CTF), abi=_CTF_ABI
        )
        cid_bytes  = bytes.fromhex(condition_id.replace("0x", ""))
        index_sets = [1] if won_side == "YES" else [2]

        calldata = ctf.encode_abi(
            "redeemPositions",
            args=[Web3.to_checksum_address(_USDC), b'\x00' * 32, cid_bytes, index_sets]
        )

        nonce = self._get_nonce()

        safe_tx = self._SafeTx(
            to=_CTF,
            operation=self._OpType.Call,
            data=calldata,
            value="0",
        )
        safe_args = self._SafeArgs(
            from_address=self._signer.address(),
            nonce=nonce,
            chain_id=137,
            transactions=[safe_tx],
        )
        tx_request = self._build_fn(
            signer=self._signer,
            args=safe_args,
            config=self._config,
            metadata=f"Redeem {slug}",
        )
        return tx_request.to_dict()

    def _sign_payment(self, pay_to: str, amount: int) -> str:
        """
        Sign a USDC transferWithAuthorization (EIP-3009).
        Signs locally, returns base64-encoded payment header.
        PK is used only for signing — not transmitted.
        """
        now   = int(time.time())
        nonce = secrets.token_bytes(32)
        msg   = {
            "from":        self._wallet,
            "to":          Web3.to_checksum_address(pay_to),
            "value":       amount,
            "validAfter":  now - 60,
            "validBefore": now + 300,
            "nonce":       nonce,
        }
        signed = self._account.sign_typed_data(_USDC_DOMAIN, _TRANSFER_TYPE, msg)

        payload = {
            "scheme":  "exact",
            "network": "eip155:137",
            "payload": {
                "from":        self._wallet,
                "to":          Web3.to_checksum_address(pay_to),
                "value":       str(amount),
                "validAfter":  str(msg["validAfter"]),
                "validBefore": str(msg["validBefore"]),
                "nonce":       "0x" + nonce.hex(),
                "v":           signed.v,
                "r":           "0x" + signed.r.to_bytes(32, "big").hex(),
                "s":           "0x" + signed.s.to_bytes(32, "big").hex(),
            }
        }
        return base64.b64encode(json.dumps(payload).encode()).decode()

    def _handle_402(self, response: requests.Response, body: dict) -> requests.Response:
        """Parse 402 response and retry with payment signature."""
        pr_raw = (
            response.headers.get("X-PAYMENT-REQUIRED") or
            response.headers.get("PAYMENT-REQUIRED") or
            response.headers.get("payment-required", "")
        )
        try:
            pr = json.loads(pr_raw)
        except Exception:
            pr = json.loads(base64.b64decode(pr_raw))

        accepts = pr.get("accepts", [])
        option  = next(
            (a for a in accepts if "137" in str(a.get("network", ""))),
            accepts[0] if accepts else None
        )
        if not option:
            raise RuntimeError("No Polygon payment option in 402 response")

        pay_to = option.get("payTo") or option.get("pay_to", "")
        raw    = option.get("amount") or option.get("price", "0.01")
        # If raw is already an integer string (e.g. "10000" = raw USDC units), use directly
        # If it's a dollar amount (e.g. "$0.01"), convert to raw units
        raw_str = str(raw).replace("$", "")
        if "." in raw_str:
            amount = int(float(raw_str) * 1_000_000)
        else:
            amount = int(raw_str)

        payment_header = self._sign_payment(pay_to, amount)

        return requests.post(
            f"{self._url}/redeem",
            headers={**self._headers, "PAYMENT-SIGNATURE": payment_header},
            json=body,
            timeout=30,
        )

    # ── Public API ────────────────────────────────────────────────────────────

    def redeem(self, slug: str, won_side: str) -> dict:
        """
        Redeem a winning Polymarket position.

        Signs the transaction locally using your private key.
        Your private key is NEVER transmitted — only the signature is sent.
        Costs $0.01 USDC, paid automatically.

        Args:
            slug:     Market slug e.g. "btc-updown-15m-1234567890"
            won_side: "YES" or "NO"

        Returns:
            {"status": "redeemed", "tx_hash": "0x...", "polygonscan": "..."}
        """
        won_side = won_side.upper()

        # Get conditionId
        condition_id = self._fetch_condition_id(slug)
        if not condition_id:
            raise ValueError(f"Market not found or conditionId unavailable: {slug}")

        # Check resolution
        if not self._is_resolved(condition_id):
            raise ValueError(f"Market not yet resolved on-chain: {slug}")

        # Build and sign tx locally
        signed_tx = self._build_signed_tx(condition_id, won_side, slug)

        body = {"signed_tx": signed_tx, "slug": slug, "won_side": won_side}

        # POST to Patly
        r = requests.post(f"{self._url}/redeem", headers=self._headers, json=body, timeout=30)

        if r.status_code == 200:
            return r.json()

        if r.status_code == 402:
            r2 = self._handle_402(r, body)
            if r2.status_code == 200:
                return r2.json()
            raise RuntimeError(f"Payment failed: {r2.status_code} {r2.text[:120]}")

        if r.status_code == 409:
            return {"status": "already_redeemed", "slug": slug}

        raise RuntimeError(f"Redeem failed: {r.status_code} {r.text[:120]}")

    def status(self) -> dict:
        """Check your account status and redeem stats."""
        r = requests.get(f"{self._url}/status", headers=self._headers, timeout=10)
        r.raise_for_status()
        return r.json()

    def history(self, limit: int = 50) -> list:
        """Get your redeem history."""
        r = requests.get(
            f"{self._url}/redeems",
            headers=self._headers,
            params={"limit": limit},
            timeout=10,
        )
        r.raise_for_status()
        return r.json()


# ── Module-level API — two functions, that's it ───────────────────────────────

_instance: Optional[Patly] = None


def init(api_key: str, pk: str, url: str = _DEFAULT_URL):
    """
    Initialize Patly. Call once at bot startup.

        import patly, os
        patly.init(api_key=os.getenv("PATLY_API_KEY"), pk=os.getenv("PK"))
    """
    global _instance
    _instance = Patly(api_key=api_key, pk=pk, url=url)
    log.info(f"[patly] initialized | wallet={_instance._wallet}")


def redeem(slug: str, won_side: str):
    """
    Redeem a winning position.

        patly.redeem("btc-updown-15m-1234567890", "NO")
    """
    if _instance is None:
        log.error("[patly] Call patly.init() first")
        return
    try:
        result = _instance.redeem(slug, won_side)
        log.info(f"[patly] ✅ {slug}: {result.get('tx_hash', result.get('status', ''))}")
    except Exception as e:
        log.error(f"[patly] ❌ {slug}: {e}")