"""
Synor Privacy SDK Client

Confidential transactions, ring signatures, stealth addresses, and commitments.
"""

import asyncio
from typing import Optional, List, Dict, Any
import httpx

from .types import (
    PrivacyConfig,
    PrivacyError,
    ConfidentialUTXO,
    ConfidentialOutput,
    ConfidentialTransaction,
    VerifyConfidentialTxResult,
    RingSignature,
    VerifyRingSignatureResult,
    StealthAddress,
    StealthKeypair,
    OneTimeAddress,
    SharedSecret,
    Commitment,
    CommitmentWithBlinding,
    RangeProof,
    VerifyRangeProofResult,
    DEFAULT_PRIVACY_ENDPOINT,
)


class SynorPrivacy:
    """
    Synor Privacy Client.

    Provides privacy-enhancing features including confidential transactions,
    ring signatures, stealth addresses, and Pedersen commitments.

    Example:
        async with SynorPrivacy(api_key="sk_...") as privacy:
            # Generate stealth keypair
            keypair = await privacy.generate_stealth_keypair()
            print(f"Stealth address: {keypair.address.address}")

            # Create ring signature
            signature = await privacy.create_ring_signature(
                message="Hello, World!",
                ring=[pub_key1, pub_key2, my_pub_key, pub_key3],
                private_key=my_private_key,
            )
    """

    def __init__(
        self,
        api_key: str,
        endpoint: str = DEFAULT_PRIVACY_ENDPOINT,
        timeout: float = 30.0,
        retries: int = 3,
        debug: bool = False,
    ):
        self.config = PrivacyConfig(
            api_key=api_key,
            endpoint=endpoint,
            timeout=timeout,
            retries=retries,
            debug=debug,
        )
        self._client = httpx.AsyncClient(
            base_url=endpoint,
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "X-SDK-Version": "python/0.1.0",
            },
            timeout=timeout,
        )
        self._closed = False

    # ==================== Confidential Transactions ====================

    async def create_confidential_transaction(
        self,
        inputs: List[ConfidentialUTXO],
        outputs: List[ConfidentialOutput],
        fee: Optional[str] = None,
        lock_height: Optional[int] = None,
    ) -> ConfidentialTransaction:
        """
        Create a confidential transaction with hidden amounts.

        Uses Pedersen commitments and Bulletproof range proofs to hide
        transaction amounts while proving they are valid (non-negative).

        Args:
            inputs: Input UTXOs with commitments
            outputs: Outputs to create
            fee: Transaction fee
            lock_height: Lock height for the transaction

        Returns:
            Confidential transaction ready for broadcast
        """
        body: Dict[str, Any] = {
            "inputs": [i.to_dict() for i in inputs],
            "outputs": [o.to_dict() for o in outputs],
        }
        if fee:
            body["fee"] = fee
        if lock_height is not None:
            body["lockHeight"] = lock_height

        response = await self._request("POST", "/transactions/confidential", body)
        return ConfidentialTransaction.from_dict(response["transaction"])

    async def verify_confidential_transaction(
        self, transaction: ConfidentialTransaction
    ) -> VerifyConfidentialTxResult:
        """
        Verify a confidential transaction.

        Checks that:
        - Commitments balance (inputs = outputs + fee)
        - All range proofs are valid (no negative amounts)
        - Kernel signature is valid
        - No duplicate inputs

        Args:
            transaction: Transaction to verify

        Returns:
            Verification result with details
        """
        response = await self._request(
            "POST",
            "/transactions/confidential/verify",
            {"transaction": transaction.raw},
        )
        return VerifyConfidentialTxResult.from_dict(response)

    async def decode_confidential_output(
        self, commitment: str, blinding: str
    ) -> str:
        """
        Decode a confidential transaction output.

        Args:
            commitment: Output commitment
            blinding: Blinding factor

        Returns:
            Decoded amount
        """
        response = await self._request(
            "POST",
            "/transactions/confidential/decode",
            {"commitment": commitment, "blinding": blinding},
        )
        return response["amount"]

    # ==================== Ring Signatures ====================

    async def create_ring_signature(
        self,
        message: str,
        ring: List[str],
        private_key: str,
        signer_index: Optional[int] = None,
    ) -> RingSignature:
        """
        Create a ring signature.

        Ring signatures allow signing a message as "one of N" without
        revealing which member of the ring actually signed.

        Args:
            message: Message to sign (hex or string)
            ring: Ring of public keys (must include signer's key)
            private_key: Signer's private key
            signer_index: Optional index of signer's key in ring

        Returns:
            Ring signature
        """
        body: Dict[str, Any] = {
            "message": message,
            "ring": ring,
            "privateKey": private_key,
        }
        if signer_index is not None:
            body["signerIndex"] = signer_index

        response = await self._request("POST", "/ring-signatures/create", body)
        return RingSignature.from_dict(response["signature"])

    async def verify_ring_signature(
        self, signature: RingSignature, message: str
    ) -> VerifyRingSignatureResult:
        """
        Verify a ring signature.

        Args:
            signature: Ring signature to verify
            message: Original message

        Returns:
            Verification result
        """
        response = await self._request(
            "POST",
            "/ring-signatures/verify",
            {"signature": signature.to_dict(), "message": message},
        )
        return VerifyRingSignatureResult.from_dict(response)

    async def is_key_image_used(self, key_image: str) -> bool:
        """
        Check if a key image has been used (double-spend detection).

        Args:
            key_image: Key image from ring signature

        Returns:
            True if the key image has been seen before
        """
        response = await self._request(
            "GET", f"/ring-signatures/key-images/{key_image}"
        )
        return response["used"]

    async def generate_random_ring(
        self, size: int, exclude: Optional[List[str]] = None
    ) -> List[str]:
        """
        Generate a random ring from available public keys.

        Args:
            size: Desired ring size
            exclude: Public keys to exclude

        Returns:
            Array of public keys for the ring
        """
        body: Dict[str, Any] = {"size": size}
        if exclude:
            body["exclude"] = exclude

        response = await self._request("POST", "/ring-signatures/random-ring", body)
        return response["ring"]

    # ==================== Stealth Addresses ====================

    async def generate_stealth_keypair(self) -> StealthKeypair:
        """
        Generate a new stealth address keypair.

        Stealth addresses allow receiving payments at unique one-time
        addresses that cannot be linked to the recipient's public address.

        Returns:
            Stealth keypair (keep private keys secure!)
        """
        response = await self._request("POST", "/stealth/generate")
        return StealthKeypair.from_dict(response["keypair"])

    async def create_one_time_address(
        self, stealth_address: StealthAddress
    ) -> OneTimeAddress:
        """
        Create a one-time address for sending to a stealth address.

        Args:
            stealth_address: Recipient's stealth address

        Returns:
            One-time address and ephemeral key
        """
        response = await self._request(
            "POST",
            "/stealth/one-time-address",
            {
                "scanPublicKey": stealth_address.scan_public_key,
                "spendPublicKey": stealth_address.spend_public_key,
            },
        )
        return OneTimeAddress.from_dict(response["oneTimeAddress"])

    async def derive_shared_secret(
        self,
        stealth_address: StealthAddress,
        private_key: str,
        ephemeral_public_key: str,
    ) -> SharedSecret:
        """
        Derive the shared secret for a stealth payment.

        Args:
            stealth_address: Stealth address
            private_key: Private key for derivation
            ephemeral_public_key: Ephemeral public key from transaction

        Returns:
            Shared secret and derived keys
        """
        response = await self._request(
            "POST",
            "/stealth/derive-secret",
            {
                "scanPublicKey": stealth_address.scan_public_key,
                "spendPublicKey": stealth_address.spend_public_key,
                "privateKey": private_key,
                "ephemeralPublicKey": ephemeral_public_key,
            },
        )
        return SharedSecret.from_dict(response)

    async def scan_for_payments(
        self,
        scan_private_key: str,
        spend_public_key: str,
        transactions: List[str],
    ) -> List[OneTimeAddress]:
        """
        Scan transactions to find stealth payments.

        Args:
            scan_private_key: Your scan private key
            spend_public_key: Your spend public key
            transactions: Transaction IDs to scan

        Returns:
            Found payments with one-time addresses
        """
        response = await self._request(
            "POST",
            "/stealth/scan",
            {
                "scanPrivateKey": scan_private_key,
                "spendPublicKey": spend_public_key,
                "transactions": transactions,
            },
        )
        return [OneTimeAddress.from_dict(p) for p in response["payments"]]

    # ==================== Pedersen Commitments ====================

    async def create_commitment(
        self, value: str, blinding: Optional[str] = None
    ) -> CommitmentWithBlinding:
        """
        Create a Pedersen commitment.

        C = vG + bH where v is the value and b is the blinding factor.

        Args:
            value: Value to commit to
            blinding: Blinding factor (generated if not provided)

        Returns:
            Commitment and blinding factor
        """
        body: Dict[str, Any] = {"value": value}
        if blinding:
            body["blinding"] = blinding

        response = await self._request("POST", "/commitments/create", body)
        return CommitmentWithBlinding.from_dict(response)

    async def open_commitment(
        self, commitment: str, value: str, blinding: str
    ) -> bool:
        """
        Open (verify) a Pedersen commitment.

        Args:
            commitment: Commitment to open
            value: Claimed value
            blinding: Blinding factor

        Returns:
            True if the commitment opens correctly
        """
        response = await self._request(
            "POST",
            "/commitments/open",
            {"commitment": commitment, "value": value, "blinding": blinding},
        )
        return response["valid"]

    async def add_commitments(self, commitment1: str, commitment2: str) -> str:
        """
        Add two commitments.

        Args:
            commitment1: First commitment
            commitment2: Second commitment

        Returns:
            Sum commitment
        """
        response = await self._request(
            "POST",
            "/commitments/add",
            {"commitment1": commitment1, "commitment2": commitment2},
        )
        return response["commitment"]

    async def subtract_commitments(self, commitment1: str, commitment2: str) -> str:
        """
        Subtract two commitments.

        Args:
            commitment1: First commitment
            commitment2: Second commitment

        Returns:
            Difference commitment
        """
        response = await self._request(
            "POST",
            "/commitments/subtract",
            {"commitment1": commitment1, "commitment2": commitment2},
        )
        return response["commitment"]

    async def compute_blinding_sum(
        self, positive: List[str], negative: List[str]
    ) -> str:
        """
        Compute blinding factor sum for transaction balancing.

        Args:
            positive: Positive blinding factors (inputs)
            negative: Negative blinding factors (outputs)

        Returns:
            Net blinding factor
        """
        response = await self._request(
            "POST",
            "/commitments/blinding-sum",
            {"positive": positive, "negative": negative},
        )
        return response["sum"]

    async def generate_blinding(self) -> str:
        """
        Generate a random blinding factor.

        Returns:
            Random 32-byte blinding factor (hex)
        """
        response = await self._request("POST", "/commitments/random-blinding")
        return response["blinding"]

    # ==================== Range Proofs ====================

    async def create_range_proof(
        self,
        value: str,
        blinding: str,
        message: Optional[str] = None,
        bit_length: int = 64,
    ) -> RangeProof:
        """
        Create a Bulletproof range proof.

        Args:
            value: Value to prove is in range
            blinding: Blinding factor
            message: Optional message to embed in proof
            bit_length: Bit length (default 64)

        Returns:
            Range proof
        """
        body: Dict[str, Any] = {
            "value": value,
            "blinding": blinding,
            "bitLength": bit_length,
        }
        if message:
            body["message"] = message

        response = await self._request("POST", "/range-proofs/create", body)
        return RangeProof.from_dict(response["proof"])

    async def verify_range_proof(
        self, commitment: str, proof: str
    ) -> VerifyRangeProofResult:
        """
        Verify a Bulletproof range proof.

        Args:
            commitment: Commitment the proof is for
            proof: Range proof data

        Returns:
            Verification result
        """
        response = await self._request(
            "POST",
            "/range-proofs/verify",
            {"commitment": commitment, "proof": proof},
        )
        return VerifyRangeProofResult.from_dict(response)

    async def create_aggregated_range_proof(
        self, outputs: List[Dict[str, str]]
    ) -> str:
        """
        Create an aggregated range proof for multiple outputs.

        Args:
            outputs: Array of {value, blinding} pairs

        Returns:
            Aggregated proof
        """
        response = await self._request(
            "POST", "/range-proofs/aggregate", {"outputs": outputs}
        )
        return response["proof"]

    # ==================== Lifecycle ====================

    async def close(self) -> None:
        """Close the client."""
        self._closed = True
        await self._client.aclose()

    def is_closed(self) -> bool:
        """Check if client is closed."""
        return self._closed

    async def health_check(self) -> bool:
        """Health check."""
        try:
            response = await self._request("GET", "/health")
            return response.get("status") == "healthy"
        except PrivacyError:
            return False

    async def __aenter__(self) -> "SynorPrivacy":
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        await self.close()

    # ==================== Private Methods ====================

    async def _request(
        self,
        method: str,
        path: str,
        body: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Make HTTP request with retries."""
        if self._closed:
            raise PrivacyError("Client has been closed")

        last_error: Optional[Exception] = None

        for attempt in range(self.config.retries):
            try:
                return await self._do_request(method, path, body)
            except PrivacyError as e:
                if self.config.debug:
                    print(f"Attempt {attempt + 1} failed: {e}")
                last_error = e
                if attempt < self.config.retries - 1:
                    await asyncio.sleep(2**attempt)

        if last_error:
            raise last_error
        raise PrivacyError("Unknown error after retries")

    async def _do_request(
        self,
        method: str,
        path: str,
        body: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Execute HTTP request."""
        try:
            response = await self._client.request(
                method,
                path,
                json=body if body else None,
            )

            if response.status_code >= 400:
                error_body = response.json() if response.content else {}
                message = (
                    error_body.get("message")
                    or error_body.get("error")
                    or f"HTTP {response.status_code}"
                )
                raise PrivacyError(
                    message,
                    code=error_body.get("code"),
                    status_code=response.status_code,
                    details=error_body,
                )

            return response.json()
        except httpx.TimeoutException as e:
            raise PrivacyError(f"Request timeout: {e}")
        except httpx.RequestError as e:
            raise PrivacyError(f"Request failed: {e}")
