"""Custodial Solana treasury adapter (ADR-0015, US-032/US-033).

The entire on-chain surface is plain SPL token operations against a single
project-owned treasury wallet: read confirmed incoming USDT transfers, send
batched payout transfers, plus devnet helpers to create the mock-USDT mint.
No Anchor programs.

RPC transport is a minimal urllib JSON-RPC client (the installed solana-py
0.40 removed its sync client); signing and instruction building use solders /
spl.token. Import this module only where a cluster is actually configured —
the deterministic local boundary in ``meshnet_contracts`` stays
dependency-free for CI.
"""

from __future__ import annotations

import base64
import json
import os
import urllib.request
from dataclasses import dataclass

from solders.hash import Hash
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.system_program import CreateAccountParams, create_account
from solders.transaction import Transaction
from spl.token.constants import TOKEN_PROGRAM_ID
from spl.token.instructions import (
    create_associated_token_account,
    get_associated_token_address,
    initialize_mint,
    mint_to,
    transfer_checked,
)
from spl.token.models import (
    InitializeMintParams,
    MintToParams,
    TransferCheckedParams,
)

USDT_DECIMALS = 6  # matches real USDT on Solana mainnet
_MINT_ACCOUNT_SIZE = 82


@dataclass(frozen=True)
class Deposit:
    """A confirmed incoming USDT transfer into the treasury token account."""

    signature: str
    sender_wallet: str
    amount_usdt: float


def load_keypair(path: str) -> Keypair:
    """Load a keypair from a solana-cli style JSON byte-array file."""
    with open(os.path.expanduser(path), encoding="utf-8") as fh:
        secret = json.load(fh)
    return Keypair.from_bytes(bytes(secret))


class RpcClient:
    """Minimal synchronous Solana JSON-RPC client."""

    def __init__(self, url: str, timeout: float = 30.0) -> None:
        self.url = url
        self._timeout = timeout
        self._request_id = 0

    def call(self, method: str, params: list | None = None):
        self._request_id += 1
        body = json.dumps({
            "jsonrpc": "2.0",
            "id": self._request_id,
            "method": method,
            "params": params or [],
        }).encode()
        request = urllib.request.Request(
            self.url,
            data=body,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        with urllib.request.urlopen(request, timeout=self._timeout) as response:
            payload = json.loads(response.read())
        if "error" in payload:
            raise RuntimeError(f"RPC {method} failed: {payload['error']}")
        return payload.get("result")


class SolanaCustodialTreasury:
    """Read deposits into / send payouts from the custodial treasury wallet."""

    def __init__(
        self,
        rpc_url: str,
        usdt_mint: str,
        treasury_keypair: Keypair | str,
        *,
        decimals: int = USDT_DECIMALS,
    ) -> None:
        self.rpc = RpcClient(rpc_url)
        self._mint = Pubkey.from_string(usdt_mint)
        self._treasury = (
            load_keypair(treasury_keypair)
            if isinstance(treasury_keypair, str)
            else treasury_keypair
        )
        self._decimals = decimals
        self._treasury_ata = get_associated_token_address(
            self._treasury.pubkey(), self._mint
        )

    @property
    def treasury_wallet(self) -> str:
        return str(self._treasury.pubkey())

    @property
    def treasury_token_account(self) -> str:
        return str(self._treasury_ata)

    # ---- deposits (US-032) ----

    def list_new_deposits(self, is_seen, *, limit: int = 200) -> list[Deposit]:
        """Confirmed incoming USDT transfers whose signature is not yet seen.

        ``is_seen(signature) -> bool`` lets the caller (the deposit watcher)
        dedupe against its ledger, so replayed and re-observed transactions
        are credited exactly once.
        """
        infos = self.rpc.call(
            "getSignaturesForAddress",
            [str(self._treasury_ata), {"limit": limit}],
        ) or []
        deposits: list[Deposit] = []
        for info in infos:
            signature = info.get("signature", "")
            if not signature or info.get("err") is not None or is_seen(signature):
                continue
            deposit = self._parse_deposit(signature)
            if deposit is not None:
                deposits.append(deposit)
        return deposits

    def _parse_deposit(self, signature: str) -> Deposit | None:
        result = self.rpc.call("getTransaction", [
            signature,
            {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0},
        ])
        meta = (result or {}).get("meta")
        if not meta:
            return None
        pre = {b["accountIndex"]: b for b in meta.get("preTokenBalances") or []}
        post = {b["accountIndex"]: b for b in meta.get("postTokenBalances") or []}

        treasury_delta = 0
        sender_wallet: str | None = None
        for index, balance in post.items():
            if balance.get("mint") != str(self._mint):
                continue
            before = pre.get(index)
            before_amount = int(before["uiTokenAmount"]["amount"]) if before else 0
            delta = int(balance["uiTokenAmount"]["amount"]) - before_amount
            owner = balance.get("owner")
            if owner == self.treasury_wallet and delta > 0:
                treasury_delta = delta
            elif delta < 0 and owner:
                sender_wallet = owner
        if sender_wallet is None:
            # a token account emptied and closed may appear only in pre balances
            for index, balance in pre.items():
                if balance.get("mint") != str(self._mint):
                    continue
                if index not in post and balance.get("owner"):
                    sender_wallet = balance["owner"]
        if treasury_delta <= 0 or sender_wallet is None:
            return None
        return Deposit(
            signature=signature,
            sender_wallet=sender_wallet,
            amount_usdt=treasury_delta / (10 ** self._decimals),
        )

    # ---- payouts (US-033) ----

    def send_payouts(self, payouts: list[tuple[str, float]]) -> str:
        """Send one batched transaction of USDT transfers treasury → wallets.

        Creates the recipient's associated token account when missing (fee
        paid by the treasury). Returns the transaction signature.
        """
        if not payouts:
            raise ValueError("payouts must be non-empty")
        instructions = []
        for wallet, amount in payouts:
            recipient = Pubkey.from_string(wallet)
            recipient_ata = get_associated_token_address(recipient, self._mint)
            if not self._account_exists(recipient_ata):
                instructions.append(create_associated_token_account(
                    payer=self._treasury.pubkey(),
                    owner=recipient,
                    mint=self._mint,
                ))
            instructions.append(transfer_checked(TransferCheckedParams(
                program_id=TOKEN_PROGRAM_ID,
                source=self._treasury_ata,
                mint=self._mint,
                dest=recipient_ata,
                owner=self._treasury.pubkey(),
                amount=int(round(amount * (10 ** self._decimals))),
                decimals=self._decimals,
            )))
        return self.send_instructions(instructions)

    # ---- shared plumbing + devnet helpers (scripts/devnet_setup.py) ----

    def _account_exists(self, pubkey: Pubkey) -> bool:
        result = self.rpc.call("getAccountInfo", [str(pubkey), {"encoding": "base64"}])
        return bool(result and result.get("value"))

    def send_instructions(self, instructions: list, extra_signers: list | None = None) -> str:
        blockhash_result = self.rpc.call("getLatestBlockhash")
        blockhash = Hash.from_string(blockhash_result["value"]["blockhash"])
        transaction = Transaction.new_signed_with_payer(
            instructions,
            self._treasury.pubkey(),
            [self._treasury, *(extra_signers or [])],
            blockhash,
        )
        encoded = base64.b64encode(bytes(transaction)).decode()
        return self.rpc.call("sendTransaction", [
            encoded,
            {"encoding": "base64", "preflightCommitment": "confirmed"},
        ])

    def get_sol_balance(self, wallet: str | None = None) -> float:
        result = self.rpc.call("getBalance", [wallet or self.treasury_wallet])
        return result["value"] / 1e9

    def request_airdrop(self, sol: float = 2.0) -> str:
        return self.rpc.call(
            "requestAirdrop", [self.treasury_wallet, int(sol * 1e9)]
        )

    def create_mock_usdt_mint(self) -> tuple["SolanaCustodialTreasury", str]:
        """Create a fresh 6-decimal mock-USDT mint (treasury = mint authority).

        Returns a new adapter bound to the created mint plus its address.
        """
        mint_keypair = Keypair()
        rent = self.rpc.call(
            "getMinimumBalanceForRentExemption", [_MINT_ACCOUNT_SIZE]
        )
        instructions = [
            create_account(CreateAccountParams(
                from_pubkey=self._treasury.pubkey(),
                to_pubkey=mint_keypair.pubkey(),
                lamports=rent,
                space=_MINT_ACCOUNT_SIZE,
                owner=TOKEN_PROGRAM_ID,
            )),
            initialize_mint(InitializeMintParams(
                program_id=TOKEN_PROGRAM_ID,
                mint=mint_keypair.pubkey(),
                decimals=self._decimals,
                mint_authority=self._treasury.pubkey(),
                freeze_authority=None,
            )),
        ]
        self.send_instructions(instructions, extra_signers=[mint_keypair])
        fresh = SolanaCustodialTreasury(
            self.rpc.url,
            str(mint_keypair.pubkey()),
            self._treasury,
            decimals=self._decimals,
        )
        return fresh, str(mint_keypair.pubkey())

    def ensure_token_account(self, wallet: str) -> str:
        owner = Pubkey.from_string(wallet)
        ata = get_associated_token_address(owner, self._mint)
        if not self._account_exists(ata):
            self.send_instructions([create_associated_token_account(
                payer=self._treasury.pubkey(), owner=owner, mint=self._mint,
            )])
        return str(ata)

    def mint_mock_usdt(self, wallet: str, amount: float) -> str:
        """Mint mock USDT to a wallet (devnet only — treasury is mint authority)."""
        ata = self.ensure_token_account(wallet)
        return self.send_instructions([mint_to(MintToParams(
            program_id=TOKEN_PROGRAM_ID,
            mint=self._mint,
            dest=Pubkey.from_string(ata),
            mint_authority=self._treasury.pubkey(),
            amount=int(round(amount * (10 ** self._decimals))),
        ))])
