# patent_2_currency_validation.py

import requests
import json
import time
import hashlib
from datetime import datetime, timedelta
from typing import Dict, Tuple, Union, Optional

class CurrencyValidator:
    """
    A class for validating currency data, detecting stale rates,
    providing confidence scores, and implementing rate limiting and caching.

    This implementation is based on Patent 2: Currency Validation System,
    covering real-time currency data verification, financial accuracy validation,
    exchange rate validation, and currency conversion verification.
    """

    def __init__(self, api_keys: Dict[str, str], sources: Dict[str, str], cache_expiry_seconds: int = 3600, rate_limit_seconds: int = 60):
        """
        Initializes the CurrencyValidator.

        Args:
            api_keys: A dictionary of API keys for different data sources (e.g., {"source1": "API_KEY_1", "source2": "API_KEY_2"}).
            sources: A dictionary of data source URLs (e.g., {"source1": "https://api.source1.com/rates", "source2": "https://api.source2.com/rates"}).
            cache_expiry_seconds: The number of seconds to cache currency data (default: 3600 seconds).
            rate_limit_seconds: The minimum time (in seconds) between API calls to a single source (default: 60 seconds).
        """
        self.api_keys = api_keys
        self.sources = sources
        self.cache_expiry_seconds = cache_expiry_seconds
        self.rate_limit_seconds = rate_limit_seconds
        self.cache: Dict[str, Dict] = {}  # Cache to store exchange rates
        self.last_called: Dict[str, float] = {}  # Track last API call timestamp per source
        self.session = requests.Session() # Use a session for connection pooling

    def _get_data_from_source(self, source_name: str, base_currency: str, target_currency: str) -> Tuple[Optional[float], float]:
        """
        Retrieves currency data from a specified source.

        Args:
            source_name: The name of the data source.
            base_currency: The base currency (e.g., "USD").
            target_currency: The target currency (e.g., "EUR").

        Returns:
            A tuple containing:
                - The exchange rate (float) if successful, otherwise None.
                - A confidence score (float) representing the reliability of the data (0.0 to 1.0).
        """
        source_url = self.sources.get(source_name)
        api_key = self.api_keys.get(source_name)

        if not source_url or not api_key:
            print(f"Error: Source URL or API key not found for {source_name}")
            return None, 0.0  # Return None and a low confidence score

        # Rate limiting
        last_called_time = self.last_called.get(source_name, 0.0)
        time_since_last_call = time.time() - last_called_time
        if time_since_last_call < self.rate_limit_seconds:
            sleep_time = self.rate_limit_seconds - time_since_last_call
            print(f"Rate limit hit for {source_name}. Sleeping for {sleep_time:.2f} seconds.")
            time.sleep(sleep_time)

        try:
            headers = {"Authorization": f"Bearer {api_key}"}  # Example header, adjust as needed
            params = {"base": base_currency, "symbols": target_currency} # Example params, adjust as needed
            response = self.session.get(source_url, headers=headers, params=params, timeout=5)  # Added timeout
            response.raise_for_status()  # Raise HTTPError for bad responses (4xx or 5xx)

            data = response.json()
            # Adapt the data extraction based on the source's API response format
            exchange_rate = self._extract_rate_from_data(data, base_currency, target_currency, source_name)

            if exchange_rate is None:
                print(f"Error: Could not extract exchange rate from {source_name} response: {data}")
                return None, 0.0

            self.last_called[source_name] = time.time()
            # Implement source-specific confidence scoring logic
            confidence_score = self._calculate_confidence_score(source_name, data)
            return exchange_rate, confidence_score

        except requests.exceptions.RequestException as e:
            print(f"Error fetching data from {source_name}: {e}")
            return None, 0.0
        except json.JSONDecodeError as e:
            print(f"Error decoding JSON from {source_name}: {e}")
            return None, 0.0
        except Exception as e:
            print(f"Unexpected error fetching data from {source_name}: {e}")
            return None, 0.0

    def _extract_rate_from_data(self, data: Dict, base_currency: str, target_currency: str, source_name: str) -> Optional[float]:
        """
        Extracts the exchange rate from the data returned by a specific source.
        This method needs to be adapted to the specific API response format of each source.

        Args:
            data: The JSON data returned by the API.
            base_currency: The base currency.
            target_currency: The target currency.
            source_name: The name of the data source.

        Returns:
            The exchange rate if found, otherwise None.
        """

        if source_name == "source1":
            # Example: Source 1 returns data in the format {"rates": {"EUR": 1.10}}
            try:
                return float(data["rates"][target_currency])
            except (KeyError, TypeError):
                return None
        elif source_name == "source2":
            # Example: Source 2 returns data in the format {"USD_EUR": 1.10}
             try:
                key = f"{base_currency}_{target_currency}"
                return float(data[key])
             except (KeyError, TypeError):
                return None
        else:
            print(f"Unknown source: {source_name}")
            return None


    def _calculate_confidence_score(self, source_name: str, data: Dict) -> float:
        """
        Calculates a confidence score based on the source and the data received.
        This is a crucial step for determining the reliability of the data.

        Args:
            source_name: The name of the data source.
            data: The JSON data returned by the API.

        Returns:
            A confidence score (float) between 0.0 and 1.0.
        """
        # Implement logic based on the source's reliability, reputation,
        # data freshness, and any error codes received.

        if source_name == "source1":
            # Example: Source 1 is generally reliable and provides timestamps.
            timestamp = data.get("timestamp") # Example data. API responses vary.
            if timestamp:
                data_age = time.time() - timestamp
                if data_age > 3600: # Older than 1 hour
                    return 0.6  # Lower confidence for older data
                else:
                    return 0.9  # High confidence for recent data
            else:
                return 0.8  # Good, but no timestamp information

        elif source_name == "source2":
            # Example: Source 2 is less reliable but has wide coverage.
            # Check for error codes or unusual values.
            error_code = data.get("error")
            if error_code:
                return 0.1 # Very low confidence due to error

            # Example: Check for unusually low/high values (potential data manipulation)
            rate = self._extract_rate_from_data(data, "USD", "EUR", source_name)
            if rate is not None and (rate < 0.5 or rate > 1.5): # Arbitrary range
                return 0.3 # Suspicious rate

            return 0.7  # Moderate confidence

        else:
            return 0.5  # Default confidence for unknown sources


    def get_exchange_rate(self, base_currency: str, target_currency: str) -> Tuple[Optional[float], float]:
        """
        Retrieves the exchange rate for a given currency pair, using multiple sources.

        Args:
            base_currency: The base currency (e.g., "USD").
            target_currency: The target currency (e.g., "EUR").

        Returns:
            A tuple containing:
                - The validated exchange rate (float) if successful, otherwise None.
                - A confidence score (float) representing the overall reliability of the data.
        """
        cache_key = self._generate_cache_key(base_currency, target_currency)

        # Check the cache first
        if cache_key in self.cache:
            cached_data = self.cache[cache_key]
            if (datetime.now() - cached_data["timestamp"]).total_seconds() < self.cache_expiry_seconds:
                print(f"Using cached data for {base_currency} to {target_currency}")
                return cached_data["rate"], cached_data["confidence"]
            else:
                print(f"Cache expired for {base_currency} to {target_currency}")
                del self.cache[cache_key]  # Remove expired entry


        rates = []
        confidence_scores = []

        for source_name in self.sources:
            rate, confidence = self._get_data_from_source(source_name, base_currency, target_currency)
            if rate is not None:
                rates.append(rate)
                confidence_scores.append(confidence)

        if not rates:
            print(f"Could not retrieve exchange rate for {base_currency} to {target_currency} from any source.")
            return None, 0.0

        # Validate and combine data from multiple sources
        validated_rate, overall_confidence = self._validate_and_combine_rates(rates, confidence_scores)

        # Store the result in the cache
        if validated_rate is not None:
            self.cache[cache_key] = {
                "rate": validated_rate,
                "confidence": overall_confidence,
                "timestamp": datetime.now()
            }

        return validated_rate, overall_confidence

    def _validate_and_combine_rates(self, rates: list[float], confidence_scores: list[float]) -> Tuple[Optional[float], float]:
        """
        Validates and combines exchange rates from multiple sources.  This is a critical component
        of the patent.  This implementation uses a weighted average based on confidence scores.  More robust
        methods could include outlier detection algorithms.

        Args:
            rates: A list of exchange rates.
            confidence_scores: A list of confidence scores corresponding to the rates.

        Returns:
            A tuple containing:
                - The validated exchange rate (float) if successful, otherwise None.
                - The overall confidence score (float).
        """

        if not rates:
            return None, 0.0

        # Simple validation: Check for outliers using interquartile range (IQR)
        q1 = sorted(rates)[len(rates) // 4]
        q3 = sorted(rates)[3 * len(rates) // 4]
        iqr = q3 - q1
        lower_bound = q1 - 1.5 * iqr
        upper_bound = q3 + 1.5 * iqr

        valid_rates = []
        valid_confidence_scores = []
        for rate, confidence in zip(rates, confidence_scores):
            if lower_bound <= rate <= upper_bound:
                valid_rates.append(rate)
                valid_confidence_scores.append(confidence)
            else:
                print(f"Outlier rate detected: {rate}. Excluding from calculation.")

        if not valid_rates:
            print("No valid rates found after outlier removal.")
            return None, 0.0

        # Weighted average based on confidence scores
        total_confidence = sum(valid_confidence_scores)
        weighted_sum = sum(rate * confidence for rate, confidence in zip(valid_rates, valid_confidence_scores))
        validated_rate = weighted_sum / total_confidence
        overall_confidence = total_confidence / len(rates) # Average confidence

        return validated_rate, overall_confidence

    def _generate_cache_key(self, base_currency: str, target_currency: str) -> str:
        """
        Generates a cache key based on the currency pair.

        Args:
            base_currency: The base currency.
            target_currency: The target currency.

        Returns:
            A unique cache key (string).
        """
        key_string = f"{base_currency}_{target_currency}"
        return hashlib.md5(key_string.encode()).hexdigest()

    def validate_currency_conversion(self, amount: float, from_currency: str, to_currency: str, expected_amount: float) -> bool:
      """
      Validates a currency conversion.

      Args:
          amount: The amount in the from_currency.
          from_currency: The currency to convert from.
          to_currency: The currency to convert to.
          expected_amount: The expected amount in the to_currency.

      Returns:
          True if the conversion is valid, False otherwise.
      """
      exchange_rate, confidence = self.get_exchange_rate(from_currency, to_currency)

      if exchange_rate is None:
          print("Could not retrieve exchange rate for validation.")
          return False

      calculated_amount = amount * exchange_rate
      # Allow for a small tolerance due to floating-point precision
      tolerance = 0.01  # 1% tolerance
      difference = abs(calculated_amount - expected_amount)
      if difference <= tolerance * expected_amount:
          return True
      else:
          print(f"Currency conversion validation failed. Calculated: {calculated_amount}, Expected: {expected_amount}")
          return False

# Example usage (replace with your actual API keys and source URLs)
if __name__ == "__main__":
    api_keys = {
        "source1": "YOUR_API_KEY_SOURCE1",
        "source2": "YOUR_API_KEY_SOURCE2"
    }
    sources = {
        "source1": "https://api.example.com/source1/rates",  # Replace with your actual API endpoint
        "source2": "https://api.example.com/source2/exchange"  # Replace with your actual API endpoint
    }

    validator = CurrencyValidator(api_keys=api_keys, sources=sources, cache_expiry_seconds=600)

    # Get exchange rate for USD to EUR
    rate, confidence = validator.get_exchange_rate("USD", "EUR")
    if rate:
        print(f"Exchange rate for USD to EUR: {rate:.4f} (Confidence: {confidence:.2f})")
    else:
        print("Could not retrieve exchange rate for USD to EUR.")

    # Validate a currency conversion
    amount_usd = 100
    expected_eur = 90  # Example expected amount
    is_valid = validator.validate_currency_conversion(amount_usd, "USD", "EUR", expected_eur)
    if is_valid:
        print("Currency conversion is valid.")
    else:
        print("Currency conversion is invalid.")

    # Test caching
    rate, confidence = validator.get_exchange_rate("USD", "EUR")
    if rate:
        print(f"Exchange rate for USD to EUR (cached): {rate:.4f} (Confidence: {confidence:.2f})")
    else:
        print("Could not retrieve exchange rate for USD to EUR.")
