"""
Oracle Aggregator - Consensus Coordinator

This service collects oracle submissions from multiple nodes, verifies consensus,
and submits the final result to the blockchain.

Flow:
1. Receive submissions from oracle nodes (via HTTP API)
2. Verify each submission's signature
3. Count votes for each proposed winner
4. Check if consensus threshold is met (e.g., 3 of 5 nodes)
5. Submit consensus result to BetOracle contract
6. If no consensus, mark request as disputed

NOTE: This is pseudocode/skeleton showing the architecture.
"""

import asyncio
from typing import Dict, List, Any, Optional
from collections import defaultdict
from datetime import datetime


class OracleSubmission:
    """Represents a single oracle node's submission."""

    def __init__(
        self,
        node_id: str,
        node_address: str,
        winner_address: str,
        result_data: Dict[str, Any],
        signature: str,
        submitted_at: datetime
    ):
        self.node_id = node_id
        self.node_address = node_address
        self.winner_address = winner_address
        self.result_data = result_data
        self.signature = signature
        self.submitted_at = submitted_at


class OracleAggregator:
    """
    Aggregates oracle submissions and achieves consensus.

    Maintains state of all active oracle requests and their submissions.
    """

    def __init__(
        self,
        blockchain_service,
        consensus_threshold: int = 3,
        total_nodes: int = 5
    ):
        """
        Initialize aggregator.

        Args:
            blockchain_service: Instance of BlockchainService
            consensus_threshold: Minimum nodes that must agree (e.g., 3)
            total_nodes: Total number of oracle nodes (e.g., 5)
        """
        self.blockchain_service = blockchain_service
        self.consensus_threshold = consensus_threshold
        self.total_nodes = total_nodes

        # Track submissions per request
        # request_id => [OracleSubmission, ...]
        self.submissions: Dict[int, List[OracleSubmission]] = defaultdict(list)

        # Track which nodes have submitted for each request
        # request_id => {node_address: bool}
        self.node_submitted: Dict[int, Dict[str, bool]] = defaultdict(dict)

        # Trusted oracle node addresses
        self.trusted_nodes = set([
            "0xNode1Address...",
            "0xNode2Address...",
            "0xNode3Address...",
            "0xNode4Address...",
            "0xNode5Address..."
        ])

    async def receive_submission(
        self,
        request_id: int,
        node_id: str,
        node_address: str,
        winner_address: str,
        result_data: Dict[str, Any],
        signature: str
    ) -> Dict[str, Any]:
        """
        Receive and process an oracle node submission.

        This is called by the HTTP API endpoint when nodes submit results.

        Args:
            request_id: Oracle request ID
            node_id: Node identifier
            node_address: Node's wallet address
            winner_address: Proposed winner address
            result_data: API result data
            signature: Node's cryptographic signature

        Returns:
            Dict with status and any consensus result
        """
        # Verify node is trusted
        if node_address not in self.trusted_nodes:
            return {
                'status': 'error',
                'message': 'Untrusted node'
            }

        # Verify node hasn't already submitted
        if self.node_submitted[request_id].get(node_address, False):
            return {
                'status': 'error',
                'message': 'Node already submitted'
            }

        # Verify signature
        is_valid = await self._verify_signature(
            request_id,
            winner_address,
            result_data,
            signature,
            node_address
        )

        if not is_valid:
            return {
                'status': 'error',
                'message': 'Invalid signature'
            }

        # Store submission
        submission = OracleSubmission(
            node_id=node_id,
            node_address=node_address,
            winner_address=winner_address,
            result_data=result_data,
            signature=signature,
            submitted_at=datetime.utcnow()
        )

        self.submissions[request_id].append(submission)
        self.node_submitted[request_id][node_address] = True

        print(f"[Aggregator] Received submission from {node_id} for request {request_id}")
        print(f"[Aggregator] {len(self.submissions[request_id])}/{self.total_nodes} nodes submitted")

        # Check if consensus threshold reached
        if len(self.submissions[request_id]) >= self.consensus_threshold:
            result = await self._check_consensus_and_settle(request_id)
            return {
                'status': 'success',
                'consensus': result
            }

        return {
            'status': 'pending',
            'submissions_count': len(self.submissions[request_id]),
            'threshold': self.consensus_threshold
        }

    async def _verify_signature(
        self,
        request_id: int,
        winner_address: str,
        result_data: Dict[str, Any],
        signature: str,
        node_address: str
    ) -> bool:
        """
        Verify that the signature is valid for this node.

        Args:
            request_id: Oracle request ID
            winner_address: Proposed winner
            result_data: API result
            signature: Signature to verify
            node_address: Expected signer address

        Returns:
            True if signature is valid
        """
        # Pseudocode:
        #
        # from eth_account.messages import encode_defunct, defunct_hash_message
        # from eth_account import Account
        # import json
        # import hashlib
        #
        # # Reconstruct message
        # message = f"{request_id}{winner_address}{json.dumps(result_data, sort_keys=True)}"
        # message_hash = hashlib.sha256(message.encode()).hexdigest()
        #
        # # Recover signer from signature
        # signable_message = encode_defunct(text=message_hash)
        # recovered_address = Account.recover_message(signable_message, signature=signature)
        #
        # # Verify signer matches node address
        # return recovered_address.lower() == node_address.lower()

        # Placeholder - assume valid
        return True

    async def _check_consensus_and_settle(self, request_id: int) -> Dict[str, Any]:
        """
        Check if consensus is reached and settle the bet.

        Steps:
        1. Count votes for each proposed winner
        2. Find winner with most votes
        3. Verify threshold met
        4. Submit to blockchain if consensus
        5. Mark as disputed if no consensus

        Args:
            request_id: Oracle request ID

        Returns:
            Dict with consensus result
        """
        submissions = self.submissions[request_id]

        # Count votes for each winner
        vote_counts: Dict[str, int] = defaultdict(int)
        vote_details: Dict[str, List[str]] = defaultdict(list)

        for submission in submissions:
            winner = submission.winner_address
            vote_counts[winner] += 1
            vote_details[winner].append(submission.node_id)

        # Find winner with most votes
        consensus_winner = max(vote_counts, key=vote_counts.get)
        consensus_votes = vote_counts[consensus_winner]

        print(f"[Aggregator] Vote results for request {request_id}:")
        for winner, count in vote_counts.items():
            nodes = vote_details[winner]
            print(f"  {winner}: {count} votes from {nodes}")

        # Check if threshold met
        if consensus_votes >= self.consensus_threshold:
            print(f"[Aggregator] Consensus reached! Winner: {consensus_winner} ({consensus_votes}/{self.total_nodes} votes)")

            # Get bet ID from request
            # In production, fetch from blockchain or database
            bet_id = await self._get_bet_id_for_request(request_id)

            # Collect signatures for blockchain submission
            signatures = [
                sub.signature
                for sub in submissions
                if sub.winner_address == consensus_winner
            ]

            # Get result data from consensus submissions
            result_data = submissions[0].result_data  # Use first submission's data

            # Submit to blockchain
            tx_hash = await self.blockchain_service.settle_bet_via_oracle(
                request_id=request_id,
                bet_id=bet_id,
                winner_address=consensus_winner,
                result_data=str(result_data).encode(),
                signatures=signatures
            )

            print(f"[Aggregator] Submitted settlement to blockchain: {tx_hash}")

            # Clean up submissions for this request
            del self.submissions[request_id]
            del self.node_submitted[request_id]

            return {
                'consensus': True,
                'winner': consensus_winner,
                'votes': consensus_votes,
                'tx_hash': tx_hash
            }

        else:
            print(f"[Aggregator] No consensus for request {request_id}")
            print(f"  Best: {consensus_winner} with {consensus_votes} votes (need {self.consensus_threshold})")

            # Mark as disputed
            await self._mark_as_disputed(request_id)

            return {
                'consensus': False,
                'reason': 'Insufficient consensus',
                'votes': dict(vote_counts)
            }

    async def _get_bet_id_for_request(self, request_id: int) -> int:
        """
        Get bet ID associated with oracle request.

        In production, this would query the BetOracle contract
        or database to get the bet_id for this request_id.

        Args:
            request_id: Oracle request ID

        Returns:
            Bet ID
        """
        # Pseudocode:
        #
        # request = await self.blockchain_service.bet_oracle_contract.functions.getRequest(
        #     request_id
        # ).call()
        #
        # return request['betId']

        # Placeholder
        return 42

    async def _mark_as_disputed(self, request_id: int):
        """
        Mark oracle request as disputed.

        Sends notification to admin for manual review.

        Args:
            request_id: Oracle request ID
        """
        # Pseudocode:
        #
        # # Update database
        # await db.execute(
        #     update(OracleRequest)
        #     .where(OracleRequest.request_id == request_id)
        #     .values(status='DISPUTED')
        # )
        #
        # # Notify admins
        # await send_admin_notification(
        #     "Oracle Dispute",
        #     f"Request {request_id} - no consensus reached"
        # )
        #
        # # Could also call blockchain contract to mark as disputed
        # await self.blockchain_service.bet_oracle_contract.functions.markAsDisputed(
        #     request_id
        # ).send()

        print(f"[Aggregator] Marked request {request_id} as disputed")

    async def get_request_status(self, request_id: int) -> Dict[str, Any]:
        """
        Get status of an oracle request.

        Args:
            request_id: Oracle request ID

        Returns:
            Dict with current status and submissions
        """
        submissions = self.submissions.get(request_id, [])

        vote_counts: Dict[str, int] = defaultdict(int)
        for sub in submissions:
            vote_counts[sub.winner_address] += 1

        return {
            'request_id': request_id,
            'submissions_count': len(submissions),
            'threshold': self.consensus_threshold,
            'votes': dict(vote_counts),
            'nodes_submitted': [sub.node_id for sub in submissions]
        }

    async def handle_timeout(self, request_id: int):
        """
        Handle oracle request timeout.

        Called if nodes fail to respond within time limit (e.g., 1 hour).

        Args:
            request_id: Oracle request ID
        """
        submissions = self.submissions.get(request_id, [])

        if len(submissions) == 0:
            print(f"[Aggregator] Request {request_id} timed out with no submissions")
        else:
            print(f"[Aggregator] Request {request_id} timed out with {len(submissions)} submissions")
            # Try to settle with available submissions
            await self._check_consensus_and_settle(request_id)

        # Mark as timed out on blockchain
        # Pseudocode:
        # await self.blockchain_service.bet_oracle_contract.functions.markAsTimedOut(
        #     request_id
        # ).send()


# Singleton instance
_aggregator: Optional[OracleAggregator] = None


def get_oracle_aggregator(blockchain_service) -> OracleAggregator:
    """Get singleton aggregator instance."""
    global _aggregator

    if _aggregator is None:
        _aggregator = OracleAggregator(
            blockchain_service=blockchain_service,
            consensus_threshold=3,
            total_nodes=5
        )

    return _aggregator


# FastAPI endpoint example
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

router = APIRouter()

class OracleSubmissionRequest(BaseModel):
    request_id: int
    node_id: str
    node_address: str
    winner_address: str
    result_data: dict
    signature: str


@router.post("/oracle/submit")
async def submit_oracle_result(submission: OracleSubmissionRequest):
    \"""
    API endpoint for oracle nodes to submit results.

    Called by oracle_node.py when a node has fetched and signed a result.
    \"""
    from app.blockchain.services.blockchain_service import get_blockchain_service

    blockchain_service = get_blockchain_service()
    aggregator = get_oracle_aggregator(blockchain_service)

    result = await aggregator.receive_submission(
        request_id=submission.request_id,
        node_id=submission.node_id,
        node_address=submission.node_address,
        winner_address=submission.winner_address,
        result_data=submission.result_data,
        signature=submission.signature
    )

    if result['status'] == 'error':
        raise HTTPException(status_code=400, detail=result['message'])

    return result


@router.get("/oracle/status/{request_id}")
async def get_oracle_status(request_id: int):
    \"""
    Get status of an oracle request.

    Shows how many nodes have submitted and current vote counts.
    \"""
    from app.blockchain.services.blockchain_service import get_blockchain_service

    blockchain_service = get_blockchain_service()
    aggregator = get_oracle_aggregator(blockchain_service)

    status = await aggregator.get_request_status(request_id)
    return status
"""
