"""
Synor Bridge SDK Client

Cross-chain asset transfers with lock-mint and burn-unlock patterns.
"""

import asyncio
from typing import Optional, List, Dict, Any
from urllib.parse import urlencode
import httpx

from .types import (
    BridgeConfig,
    BridgeError,
    Chain,
    ChainId,
    Asset,
    WrappedAsset,
    Transfer,
    TransferStatus,
    TransferFilter,
    LockReceipt,
    LockProof,
    LockOptions,
    BurnReceipt,
    BurnProof,
    BurnOptions,
    MintOptions,
    UnlockOptions,
    FeeEstimate,
    ExchangeRate,
    SignedTransaction,
    DEFAULT_BRIDGE_ENDPOINT,
)


class SynorBridge:
    """Synor Bridge Client for cross-chain transfers."""

    def __init__(
        self,
        api_key: str,
        endpoint: str = DEFAULT_BRIDGE_ENDPOINT,
        timeout: float = 60.0,
        retries: int = 3,
        debug: bool = False,
    ):
        self.config = BridgeConfig(
            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

    # ==================== Chain Operations ====================

    async def get_supported_chains(self) -> List[Chain]:
        """Get all supported chains."""
        response = await self._request("GET", "/chains")
        return [Chain.from_dict(c) for c in response.get("chains", [])]

    async def get_chain(self, chain_id: ChainId) -> Chain:
        """Get chain by ID."""
        response = await self._request("GET", f"/chains/{chain_id}")
        return Chain.from_dict(response)

    async def is_chain_supported(self, chain_id: ChainId) -> bool:
        """Check if chain is supported."""
        try:
            chain = await self.get_chain(chain_id)
            return chain.supported
        except BridgeError:
            return False

    # ==================== Asset Operations ====================

    async def get_supported_assets(self, chain_id: ChainId) -> List[Asset]:
        """Get supported assets for a chain."""
        response = await self._request("GET", f"/chains/{chain_id}/assets")
        return [Asset.from_dict(a) for a in response.get("assets", [])]

    async def get_asset(self, asset_id: str) -> Asset:
        """Get asset by ID."""
        response = await self._request("GET", f"/assets/{asset_id}")
        return Asset.from_dict(response)

    async def get_wrapped_asset(
        self, original_asset_id: str, target_chain: ChainId
    ) -> WrappedAsset:
        """Get wrapped asset mapping."""
        response = await self._request(
            "GET", f"/assets/{original_asset_id}/wrapped/{target_chain}"
        )
        return WrappedAsset.from_dict(response)

    async def get_wrapped_assets(self, chain_id: ChainId) -> List[WrappedAsset]:
        """Get all wrapped assets for a chain."""
        response = await self._request("GET", f"/chains/{chain_id}/wrapped")
        return [WrappedAsset.from_dict(a) for a in response.get("assets", [])]

    # ==================== Fee & Rate Operations ====================

    async def estimate_fee(
        self,
        asset: str,
        amount: str,
        source_chain: ChainId,
        target_chain: ChainId,
    ) -> FeeEstimate:
        """Estimate bridge fee."""
        response = await self._request(
            "POST",
            "/fees/estimate",
            {
                "asset": asset,
                "amount": amount,
                "sourceChain": source_chain,
                "targetChain": target_chain,
            },
        )
        return FeeEstimate.from_dict(response)

    async def get_exchange_rate(
        self, from_asset: str, to_asset: str
    ) -> ExchangeRate:
        """Get exchange rate between assets."""
        response = await self._request("GET", f"/rates/{from_asset}/{to_asset}")
        return ExchangeRate.from_dict(response)

    # ==================== Lock-Mint Flow ====================

    async def lock(
        self,
        asset: str,
        amount: str,
        target_chain: ChainId,
        options: Optional[LockOptions] = None,
    ) -> LockReceipt:
        """
        Lock assets on source chain for cross-chain transfer.
        Step 1 of lock-mint flow.
        """
        body: Dict[str, Any] = {
            "asset": asset,
            "amount": amount,
            "targetChain": target_chain,
        }
        if options:
            if options.recipient:
                body["recipient"] = options.recipient
            if options.deadline:
                body["deadline"] = options.deadline
            if options.slippage is not None:
                body["slippage"] = options.slippage

        response = await self._request("POST", "/transfers/lock", body)
        return LockReceipt.from_dict(response)

    async def get_lock_proof(self, lock_receipt_id: str) -> LockProof:
        """
        Get lock proof for minting.
        Step 2 of lock-mint flow (wait for confirmations).
        """
        response = await self._request(
            "GET", f"/transfers/lock/{lock_receipt_id}/proof"
        )
        return LockProof.from_dict(response)

    async def wait_for_lock_proof(
        self,
        lock_receipt_id: str,
        poll_interval: float = 5.0,
        max_wait: float = 600.0,
    ) -> LockProof:
        """
        Wait for lock proof to be ready.
        Polls until confirmations are sufficient.
        """
        elapsed = 0.0
        while elapsed < max_wait:
            try:
                return await self.get_lock_proof(lock_receipt_id)
            except BridgeError as e:
                if e.code == "CONFIRMATIONS_PENDING":
                    await asyncio.sleep(poll_interval)
                    elapsed += poll_interval
                    continue
                raise

        raise BridgeError(
            "Timeout waiting for lock proof",
            code="CONFIRMATIONS_PENDING",
        )

    async def mint(
        self,
        proof: LockProof,
        target_address: str,
        options: Optional[MintOptions] = None,
    ) -> SignedTransaction:
        """
        Mint wrapped tokens on target chain.
        Step 3 of lock-mint flow.
        """
        body: Dict[str, Any] = {
            "proof": proof.to_dict(),
            "targetAddress": target_address,
        }
        if options:
            if options.gas_limit:
                body["gasLimit"] = options.gas_limit
            if options.max_fee_per_gas:
                body["maxFeePerGas"] = options.max_fee_per_gas
            if options.max_priority_fee_per_gas:
                body["maxPriorityFeePerGas"] = options.max_priority_fee_per_gas

        response = await self._request("POST", "/transfers/mint", body)
        return SignedTransaction.from_dict(response)

    # ==================== Burn-Unlock Flow ====================

    async def burn(
        self,
        wrapped_asset: str,
        amount: str,
        options: Optional[BurnOptions] = None,
    ) -> BurnReceipt:
        """
        Burn wrapped tokens on current chain.
        Step 1 of burn-unlock flow.
        """
        body: Dict[str, Any] = {
            "wrappedAsset": wrapped_asset,
            "amount": amount,
        }
        if options:
            if options.recipient:
                body["recipient"] = options.recipient
            if options.deadline:
                body["deadline"] = options.deadline

        response = await self._request("POST", "/transfers/burn", body)
        return BurnReceipt.from_dict(response)

    async def get_burn_proof(self, burn_receipt_id: str) -> BurnProof:
        """
        Get burn proof for unlocking.
        Step 2 of burn-unlock flow (wait for confirmations).
        """
        response = await self._request(
            "GET", f"/transfers/burn/{burn_receipt_id}/proof"
        )
        return BurnProof.from_dict(response)

    async def wait_for_burn_proof(
        self,
        burn_receipt_id: str,
        poll_interval: float = 5.0,
        max_wait: float = 600.0,
    ) -> BurnProof:
        """
        Wait for burn proof to be ready.
        Polls until confirmations are sufficient.
        """
        elapsed = 0.0
        while elapsed < max_wait:
            try:
                return await self.get_burn_proof(burn_receipt_id)
            except BridgeError as e:
                if e.code == "CONFIRMATIONS_PENDING":
                    await asyncio.sleep(poll_interval)
                    elapsed += poll_interval
                    continue
                raise

        raise BridgeError(
            "Timeout waiting for burn proof",
            code="CONFIRMATIONS_PENDING",
        )

    async def unlock(
        self,
        proof: BurnProof,
        options: Optional[UnlockOptions] = None,
    ) -> SignedTransaction:
        """
        Unlock original tokens on source chain.
        Step 3 of burn-unlock flow.
        """
        body: Dict[str, Any] = {"proof": proof.to_dict()}
        if options:
            if options.gas_limit:
                body["gasLimit"] = options.gas_limit
            if options.gas_price:
                body["gasPrice"] = options.gas_price

        response = await self._request("POST", "/transfers/unlock", body)
        return SignedTransaction.from_dict(response)

    # ==================== Transfer Management ====================

    async def get_transfer(self, transfer_id: str) -> Transfer:
        """Get transfer by ID."""
        response = await self._request("GET", f"/transfers/{transfer_id}")
        return Transfer.from_dict(response)

    async def get_transfer_status(self, transfer_id: str) -> TransferStatus:
        """Get transfer status."""
        transfer = await self.get_transfer(transfer_id)
        return transfer.status

    async def list_transfers(
        self, filter: Optional[TransferFilter] = None
    ) -> List[Transfer]:
        """List transfers with optional filters."""
        params: Dict[str, str] = {}
        if filter:
            if filter.status:
                params["status"] = filter.status.value
            if filter.source_chain:
                params["sourceChain"] = filter.source_chain
            if filter.target_chain:
                params["targetChain"] = filter.target_chain
            if filter.asset:
                params["asset"] = filter.asset
            if filter.sender:
                params["sender"] = filter.sender
            if filter.recipient:
                params["recipient"] = filter.recipient
            if filter.from_date is not None:
                params["fromDate"] = str(filter.from_date)
            if filter.to_date is not None:
                params["toDate"] = str(filter.to_date)
            if filter.limit is not None:
                params["limit"] = str(filter.limit)
            if filter.offset is not None:
                params["offset"] = str(filter.offset)

        path = "/transfers"
        if params:
            path = f"/transfers?{urlencode(params)}"

        response = await self._request("GET", path)
        return [Transfer.from_dict(t) for t in response.get("transfers", [])]

    async def wait_for_transfer(
        self,
        transfer_id: str,
        poll_interval: float = 10.0,
        max_wait: float = 1800.0,
    ) -> Transfer:
        """Wait for transfer to complete."""
        final_statuses = {
            TransferStatus.COMPLETED,
            TransferStatus.FAILED,
            TransferStatus.REFUNDED,
        }
        elapsed = 0.0

        while elapsed < max_wait:
            transfer = await self.get_transfer(transfer_id)
            if transfer.status in final_statuses:
                return transfer
            await asyncio.sleep(poll_interval)
            elapsed += poll_interval

        raise BridgeError("Timeout waiting for transfer completion")

    # ==================== Convenience Methods ====================

    async def bridge_to(
        self,
        asset: str,
        amount: str,
        target_chain: ChainId,
        target_address: str,
        lock_options: Optional[LockOptions] = None,
        mint_options: Optional[MintOptions] = None,
    ) -> Transfer:
        """
        Execute complete lock-mint transfer.
        Combines lock, wait for proof, and mint into single operation.
        """
        # Lock on source chain
        lock_receipt = await self.lock(asset, amount, target_chain, lock_options)

        if self.config.debug:
            print(f"Locked: {lock_receipt.id}, waiting for confirmations...")

        # Wait for proof
        proof = await self.wait_for_lock_proof(lock_receipt.id)

        if self.config.debug:
            print(f"Proof ready, minting on {target_chain}...")

        # Mint on target chain
        await self.mint(proof, target_address, mint_options)

        # Return final transfer status
        return await self.wait_for_transfer(lock_receipt.id)

    async def bridge_back(
        self,
        wrapped_asset: str,
        amount: str,
        burn_options: Optional[BurnOptions] = None,
        unlock_options: Optional[UnlockOptions] = None,
    ) -> Transfer:
        """
        Execute complete burn-unlock transfer.
        Combines burn, wait for proof, and unlock into single operation.
        """
        # Burn wrapped tokens
        burn_receipt = await self.burn(wrapped_asset, amount, burn_options)

        if self.config.debug:
            print(f"Burned: {burn_receipt.id}, waiting for confirmations...")

        # Wait for proof
        proof = await self.wait_for_burn_proof(burn_receipt.id)

        if self.config.debug:
            print(f"Proof ready, unlocking on {burn_receipt.target_chain}...")

        # Unlock on original chain
        await self.unlock(proof, unlock_options)

        # Return final transfer status
        return await self.wait_for_transfer(burn_receipt.id)

    # ==================== 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 BridgeError:
            return False

    async def __aenter__(self) -> "SynorBridge":
        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 BridgeError("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 BridgeError 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 BridgeError("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 BridgeError(
                    message,
                    code=error_body.get("code"),
                    status_code=response.status_code,
                    details=error_body,
                )

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