#!/usr/bin/env python3
"""
Oracle Publisher Service
Fetches data from multiple sources, aggregates it, and posts updates to the blockchain
"""

import os
import asyncio
import logging
import time
from typing import List, Optional
from decimal import Decimal
from dataclasses import dataclass, field
from web3 import Web3
from web3.middleware import geth_poa_middleware
from eth_account import Account
import requests
from prometheus_client import Counter, Gauge, start_http_server

# OpenTelemetry instrumentation (optional)
try:
    from opentelemetry_instrumentation import setup_tracing
    tracer = setup_tracing()
    TRACING_ENABLED = True
except ImportError:
    TRACING_ENABLED = False
    tracer = None

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Prometheus metrics
updates_sent = Counter('oracle_updates_sent_total', 'Total number of oracle updates sent')
update_errors = Counter('oracle_update_errors_total', 'Total number of oracle update errors')
current_price = Gauge('oracle_current_price', 'Current oracle price')
price_deviation = Gauge('oracle_price_deviation', 'Price deviation from last update')


@dataclass
class DataSource:
    """Data source configuration"""
    name: str
    url: str
    parser: str  # JSON path or function name
    weight: float = 1.0


@dataclass
class OracleConfig:
    """Oracle configuration"""
    rpc_url: str
    aggregator_address: str
    private_key: str
    heartbeat: int = 60  # seconds
    deviation_threshold: float = 0.5  # percentage
    data_sources: List[DataSource] = field(default_factory=list)
    gas_price: Optional[int] = None
    gas_limit: int = 100000
    max_priority_fee: Optional[int] = None


class OraclePublisher:
    """Oracle publisher service"""
    
    def __init__(self, config: OracleConfig):
        self.config = config
        self.w3 = Web3(Web3.HTTPProvider(config.rpc_url))
        self.w3.middleware_onion.inject(geth_poa_middleware, layer=0)
        self.account = Account.from_key(config.private_key)
        self.last_price: Optional[Decimal] = None
        self.last_update_time: float = 0
        self.aggregator_abi = self._load_aggregator_abi()
        self.aggregator_contract = self.w3.eth.contract(
            address=Web3.to_checksum_address(config.aggregator_address),
            abi=self.aggregator_abi
        )
        
    def _load_aggregator_abi(self) -> List[dict]:
        """Load aggregator ABI"""
        # Simplified ABI for updateAnswer function
        return [
            {
                "inputs": [{"internalType": "uint256", "name": "answer", "type": "uint256"}],
                "name": "updateAnswer",
                "outputs": [],
                "stateMutability": "nonpayable",
                "type": "function"
            },
            {
                "inputs": [],
                "name": "latestAnswer",
                "outputs": [{"internalType": "int256", "name": "", "type": "int256"}],
                "stateMutability": "view",
                "type": "function"
            },
            {
                "inputs": [],
                "name": "latestRound",
                "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
                "stateMutability": "view",
                "type": "function"
            }
        ]
    
    async def fetch_price_from_source(self, source: DataSource) -> Optional[Decimal]:
        """Fetch price from a data source"""
        try:
            response = requests.get(source.url, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            # Simple JSON path parser (can be enhanced)
            price_str = self._parse_price(data, source.parser)
            if price_str:
                return Decimal(price_str)
        except Exception as e:
            logger.error(f"Error fetching price from {source.name}: {e}")
        return None
    
    def _parse_price(self, data: dict, parser: str) -> Optional[str]:
        """Parse price from JSON data"""
        # Simple implementation - can be enhanced with jsonpath
        try:
            keys = parser.split('.')
            value = data
            for key in keys:
                value = value[key]
            return str(value)
        except (KeyError, TypeError):
            return None
    
    async def fetch_aggregated_price(self) -> Optional[Decimal]:
        """Fetch and aggregate prices from multiple sources"""
        prices = []
        weights = []
        
        for source in self.config.data_sources:
            price = await self.fetch_price_from_source(source)
            if price:
                prices.append(price)
                weights.append(source.weight)
                logger.info(f"Price from {source.name}: {price}")
        
        if not prices:
            logger.error("No prices fetched from any source")
            return None
        
        # Weighted median (simplified - calculate median)
        if len(prices) == 1:
            return prices[0]
        
        # Sort prices
        sorted_prices = sorted(prices)
        n = len(sorted_prices)
        
        if n % 2 == 0:
            median = (sorted_prices[n // 2 - 1] + sorted_prices[n // 2]) / 2
        else:
            median = sorted_prices[n // 2]
        
        logger.info(f"Aggregated price (median): {median}")
        return median
    
    def should_update(self, new_price: Decimal) -> bool:
        """Check if price should be updated"""
        current_time = time.time()
        
        # Check heartbeat
        if current_time - self.last_update_time >= self.config.heartbeat:
            logger.info("Update required: heartbeat elapsed")
            return True
        
        # Check deviation threshold
        if self.last_price is None:
            return True
        
        deviation = abs((new_price - self.last_price) / self.last_price * 100)
        if deviation >= self.config.deviation_threshold:
            logger.info(f"Update required: deviation {deviation:.2f}% >= {self.config.deviation_threshold}%")
            return True
        
        logger.debug(f"Update not required: deviation {deviation:.2f}% < {self.config.deviation_threshold}%")
        return False
    
    async def update_oracle(self, price: Decimal) -> bool:
        """Update oracle on blockchain"""
        try:
            # Convert price to uint256 (assuming 8 decimals)
            price_int = int(price * Decimal(10**8))
            
            # Build transaction
            nonce = self.w3.eth.get_transaction_count(self.account.address)
            
            # Get gas price
            if self.config.gas_price:
                gas_price = self.config.gas_price
            else:
                gas_price = self.w3.eth.gas_price

            for attempt in range(3):
                bumped_gas = int(gas_price * (1.15 ** attempt))
                transaction = self.aggregator_contract.functions.updateAnswer(price_int).build_transaction({
                    'from': self.account.address,
                    'nonce': nonce,
                    'gas': self.config.gas_limit,
                    'gasPrice': bumped_gas,
                    'chainId': 138
                })

                signed_txn = self.account.sign_transaction(transaction)

                try:
                    tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
                    logger.info(f"Transaction sent: {tx_hash.hex()} (gasPrice={bumped_gas})")
                    receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
                    if receipt.status == 1:
                        logger.info(f"Transaction confirmed: {tx_hash.hex()}")
                        updates_sent.inc()
                        current_price.set(float(price))
                        self.last_price = price
                        self.last_update_time = time.time()
                        return True

                    logger.error(f"Transaction failed: {tx_hash.hex()}")
                    update_errors.inc()
                    return False
                except Exception as send_error:
                    message = str(send_error).lower()
                    if 'underpriced' in message and attempt < 2:
                        logger.warning(f"Replacement underpriced at gasPrice={bumped_gas}; retrying with higher gas")
                        continue
                    raise send_error

            return False
        except Exception as e:
            logger.error(f"Error updating oracle: {e}")
            update_errors.inc()
            return False
    
    async def run(self):
        """Main loop"""
        logger.info("Starting oracle publisher service")
        logger.info(f"RPC URL: {self.config.rpc_url}")
        logger.info(f"Aggregator address: {self.config.aggregator_address}")
        logger.info(f"Heartbeat: {self.config.heartbeat}s")
        logger.info(f"Deviation threshold: {self.config.deviation_threshold}%")
        
        # Start Prometheus metrics server
        start_http_server(8000)
        logger.info("Prometheus metrics server started on port 8000")
        
        while True:
            try:
                # Fetch aggregated price
                price = await self.fetch_aggregated_price()
                
                if price is None:
                    logger.warning("Failed to fetch price, retrying in 10 seconds")
                    await asyncio.sleep(10)
                    continue
                
                # Check if update is needed
                if self.should_update(price):
                    # Update oracle
                    success = await self.update_oracle(price)
                    if success:
                        logger.info(f"Oracle updated successfully: {price}")
                    else:
                        logger.error("Failed to update oracle")
                else:
                    logger.debug(f"Update not required: {price}")
                
                # Wait before next iteration
                await asyncio.sleep(10)
                
            except KeyboardInterrupt:
                logger.info("Shutting down oracle publisher service")
                break
            except Exception as e:
                logger.error(f"Error in main loop: {e}")
                await asyncio.sleep(10)


def load_config() -> OracleConfig:
    """Load configuration from environment variables"""
    return OracleConfig(
        rpc_url=os.getenv("RPC_URL", "http://localhost:8545"),
        aggregator_address=os.getenv("AGGREGATOR_ADDRESS", ""),
        private_key=os.getenv("PRIVATE_KEY", ""),
        heartbeat=int(os.getenv("HEARTBEAT", "60")),
        deviation_threshold=float(os.getenv("DEVIATION_THRESHOLD", "0.5")),
        data_sources=[
            DataSource(
                name="source1",
                url=os.getenv("DATA_SOURCE_1_URL", ""),
                parser=os.getenv("DATA_SOURCE_1_PARSER", "price"),
                weight=1.0
            ),
            DataSource(
                name="source2",
                url=os.getenv("DATA_SOURCE_2_URL", ""),
                parser=os.getenv("DATA_SOURCE_2_PARSER", "price"),
                weight=1.0
            ),
        ],
        gas_limit=int(os.getenv("GAS_LIMIT", "100000")),
        gas_price=int(os.getenv("GAS_PRICE", "0")) or None,
        max_priority_fee=int(os.getenv("MAX_PRIORITY_FEE", "0")) or None
    )


async def main():
    """Main entry point"""
    config = load_config()
    
    if not config.aggregator_address:
        logger.error("AGGREGATOR_ADDRESS environment variable not set")
        return
    
    if not config.private_key:
        logger.error("PRIVATE_KEY environment variable not set")
        return
    
    publisher = OraclePublisher(config)
    await publisher.run()


if __name__ == "__main__":
    asyncio.run(main())

