"""Sealed-bid auctions over Skytale encrypted channels.

Implements a commit-reveal auction protocol where agents submit
hashed bids, then reveal them. All operations happen over MLS
channels -- no smart contract required for the basic protocol.

Usage::

    from skytale_sdk.auction import SealedBidAuction

    auction = SealedBidAuction(manager, "org/ns/auction")
    commitment = auction.commit(bid_amount=1500, metadata={"asset": "ETH/USDC"})
    # ... wait for all participants to commit ...
    auction.reveal(commitment)
    result = auction.resolve()
"""

from __future__ import annotations

import hashlib
import json
import os
import time
from dataclasses import dataclass, field
from typing import Optional

from skytale_sdk.channels import SkytaleChannelManager


@dataclass
class Commitment:
    """A sealed bid commitment.

    Attributes:
        bid_amount: The bid value (interpretation depends on auction type).
        salt: Random salt used for the commitment hash.
        commitment_hash: SHA-256 hash of bid || salt.
        metadata: Optional metadata attached to the bid.
        timestamp: Unix timestamp when the commitment was created.
    """

    bid_amount: int
    salt: bytes
    commitment_hash: str
    metadata: dict = field(default_factory=dict)
    timestamp: float = field(default_factory=time.time)


@dataclass
class Reveal:
    """A revealed bid from a participant.

    Attributes:
        sender: Sender identity (from channel message metadata).
        bid_amount: The revealed bid value.
        salt_hex: Hex-encoded salt.
        commitment_hash: Hash that was committed.
        metadata: Optional metadata from the bid.
        valid: Whether the reveal matches the commitment.
    """

    sender: str
    bid_amount: int
    salt_hex: str
    commitment_hash: str
    metadata: dict = field(default_factory=dict)
    valid: bool = False


@dataclass
class AuctionResult:
    """Result of a resolved auction.

    Attributes:
        winner: Identity of the winning bidder (highest valid bid).
        winning_bid: The winning bid amount.
        reveals: All valid reveals, sorted by bid amount descending.
        invalid_reveals: Reveals that failed verification.
    """

    winner: Optional[str]
    winning_bid: int
    reveals: list[Reveal] = field(default_factory=list)
    invalid_reveals: list[Reveal] = field(default_factory=list)


def _compute_hash(bid_amount: int, salt: bytes, metadata: dict) -> str:
    """Compute SHA-256 commitment hash."""
    payload = json.dumps(
        {"bid": bid_amount, "salt": salt.hex(), "meta": metadata},
        sort_keys=True,
        separators=(",", ":"),
    ).encode()
    return hashlib.sha256(payload).hexdigest()


class SealedBidAuction:
    """Sealed-bid auction over a Skytale encrypted channel.

    All communication happens over MLS-encrypted channels. The relay
    never sees bid values -- only ciphertext.

    Args:
        manager: Channel manager for sending/receiving messages.
        channel: Channel name for the auction.
        highest_wins: If ``True`` (default), highest bid wins.
            Set to ``False`` for reverse auctions (lowest bid wins).

    Example::

        auction = SealedBidAuction(mgr, "fund/auctions/lot-42")

        # Phase 1: Commit
        commitment = auction.commit(bid_amount=1500)

        # Phase 2: Reveal (after all participants have committed)
        auction.reveal(commitment)

        # Phase 3: Resolve (after all participants have revealed)
        result = auction.resolve()
        print(f"Winner: {result.winner} with bid {result.winning_bid}")
    """

    def __init__(
        self,
        manager: SkytaleChannelManager,
        channel: str,
        highest_wins: bool = True,
    ):
        self._mgr = manager
        self._channel = channel
        self._highest_wins = highest_wins
        self._commitments: dict[str, str] = {}  # hash -> raw message
        self._reveals: list[Reveal] = []

    def commit(self, bid_amount: int, metadata: Optional[dict] = None) -> Commitment:
        """Create and broadcast a sealed bid commitment.

        Generates a random salt, hashes the bid, and sends the
        commitment hash to the auction channel.

        Args:
            bid_amount: The bid value.
            metadata: Optional metadata (e.g. ``{"asset": "ETH/USDC"}``).

        Returns:
            A :class:`Commitment` to use in the reveal phase.
        """
        meta = metadata or {}
        salt = os.urandom(32)
        commitment_hash = _compute_hash(bid_amount, salt, meta)

        msg = json.dumps({
            "type": "commit",
            "hash": commitment_hash,
            "timestamp": time.time(),
        })
        self._mgr.send(self._channel, msg)

        return Commitment(
            bid_amount=bid_amount,
            salt=salt,
            commitment_hash=commitment_hash,
            metadata=meta,
        )

    def reveal(self, commitment: Commitment) -> None:
        """Broadcast the bid reveal for a previous commitment.

        Args:
            commitment: The commitment returned by :meth:`commit`.
        """
        msg = json.dumps({
            "type": "reveal",
            "bid": commitment.bid_amount,
            "salt": commitment.salt.hex(),
            "hash": commitment.commitment_hash,
            "meta": commitment.metadata,
            "timestamp": time.time(),
        })
        self._mgr.send(self._channel, msg)

    def collect(self, timeout: float = 10.0) -> None:
        """Collect messages from the channel (commitments and reveals).

        Call this between phases to gather messages from other participants.

        Args:
            timeout: Seconds to wait for messages.
        """
        msgs = self._mgr.receive(self._channel, timeout=timeout)
        for raw in msgs:
            try:
                data = json.loads(raw)
            except (json.JSONDecodeError, TypeError):
                continue

            msg_type = data.get("type")
            if msg_type == "commit":
                self._commitments[data["hash"]] = raw
            elif msg_type == "reveal":
                expected_hash = _compute_hash(
                    data["bid"], bytes.fromhex(data["salt"]), data.get("meta", {})
                )
                valid = expected_hash == data.get("hash", "")
                reveal = Reveal(
                    sender=data.get("sender", "unknown"),
                    bid_amount=data["bid"],
                    salt_hex=data["salt"],
                    commitment_hash=data.get("hash", ""),
                    metadata=data.get("meta", {}),
                    valid=valid,
                )
                self._reveals.append(reveal)

    def resolve(self) -> AuctionResult:
        """Resolve the auction and determine the winner.

        Call after all participants have committed and revealed.
        Returns the auction result with the winner and all valid bids.

        Returns:
            :class:`AuctionResult` with the winner and sorted bids.
        """
        valid = [r for r in self._reveals if r.valid]
        invalid = [r for r in self._reveals if not r.valid]

        valid.sort(key=lambda r: r.bid_amount, reverse=self._highest_wins)

        winner = valid[0].sender if valid else None
        winning_bid = valid[0].bid_amount if valid else 0

        return AuctionResult(
            winner=winner,
            winning_bid=winning_bid,
            reveals=valid,
            invalid_reveals=invalid,
        )
