"""Abstract base class for discount processors with template method pattern."""

from abc import ABC, abstractmethod
from typing import List, Set, Tuple
import numpy as np
import polars as pl

from ....log_context.core import get_logger


class DiscountProcessor(ABC):
    """Abstract base class for discount processors with optimized common operations.
    
    Uses template method pattern to provide consistent discount application workflow
    while allowing subclasses to implement provider-specific logic.
    """

    def __init__(self, discount_percentage: float = 0.15, discount_range: tuple = (0.1, 0.3)):
        """Initialize discount processor with configurable parameters.
        
        Args:
            discount_percentage: Percentage of eligible resources to discount (default 15%)
            discount_range: Min/max discount factor range (default 10-30% discount)
        """
        self.discount_percentage = discount_percentage
        self.discount_range = discount_range
        self.logger = get_logger(__name__)

    @abstractmethod
    def get_relevant_services(self) -> List[str]:
        """Get the list of services relevant for this discount type.
        
        Returns:
            List of service names that are eligible for this discount
        """
        pass

    @abstractmethod
    def _apply_specific_discount(self, data: pl.DataFrame, discount_mask: pl.Series) -> pl.DataFrame:
        """Apply the specific discount logic to the selected rows.

        Args:
            data: Input DataFrame
            discount_mask: Boolean mask of rows to apply discount to

        Returns:
            DataFrame with specific discount applied
        """
        pass

    def get_additional_filters(self, data: pl.DataFrame) -> pl.Series:
        """Get additional filtering criteria beyond service filtering.
        
        Subclasses can override this method to add provider-specific filtering
        such as pricing unit requirements.
        
        Args:
            data: Input DataFrame to filter
            
        Returns:
            Boolean Series mask for additional filtering criteria
        """
        # Default implementation: no additional filters (all records pass)
        return pl.Series([True] * len(data))

    def apply_discount(
        self, data: pl.DataFrame, already_discounted: Set[str]
    ) -> Tuple[pl.DataFrame, Set[str]]:
        """
        Apply discounts to billing data using template method pattern.

        Args:
            data: Input billing data
            already_discounted: Set of resource IDs that already have discounts applied

        Returns:
            Tuple containing:
            - DataFrame with discounts applied
            - Set of newly discounted resource IDs
        """
        if data.is_empty():
            return data, set()

        result = data.clone()
        newly_discounted = set()
        
        # Debug: Check available columns
        self.logger.debug(
            f"Available columns in {self.__class__.__name__}",
            columns=result.columns,
            column_count=len(result.columns)
        )
        
        # Check required columns exist
        required_columns = ["service", "resource_id"]
        missing_columns = [col for col in required_columns if col not in result.columns]
        if missing_columns:
            raise ValueError(f"Missing required columns for discount processing: {missing_columns}. Available columns: {result.columns}")

        # Step 1: Filter for relevant services
        relevant_services = self.get_relevant_services()
        service_mask = result.select(
            pl.col("service").is_in(relevant_services)
        ).to_series()

        if not service_mask.any():
            self.logger.debug(
                f"No relevant services found for {self.__class__.__name__}",
                relevant_services=relevant_services
            )
            return result, newly_discounted

        # Step 2: Apply additional filters (e.g., pricing unit)
        additional_filters_mask = self.get_additional_filters(result)

        # Step 3: Identify eligible resources (not already discounted)
        eligible_resources_mask = result.select(
            ~pl.col("resource_id").is_in(list(already_discounted))
        ).to_series()
        combined_mask = service_mask & additional_filters_mask & eligible_resources_mask

        if not combined_mask.any():
            self.logger.debug(
                f"No eligible resources found for {self.__class__.__name__}",
                already_discounted_count=len(already_discounted),
                service_matches=service_mask.sum(),
                additional_filter_matches=additional_filters_mask.sum()
            )
            return result, newly_discounted

        # Step 4: Select resources for discount application
        eligible_resources = (
            result.filter(combined_mask)
            .select("resource_id")
            .unique()
            .to_series()
            .to_list()
        )

        # Calculate number of resources to discount
        discount_count = max(1, int(len(eligible_resources) * self.discount_percentage))
        discount_count = min(discount_count, len(eligible_resources))
        
        # Use deterministic random selection for reproducibility
        np.random.seed(hash(str(sorted(eligible_resources))) % (2**32))
        resources_to_discount = np.random.choice(
            eligible_resources, size=discount_count, replace=False
        )

        # Track newly discounted resources
        newly_discounted.update(resources_to_discount)

        # Step 5: Ensure consistent schema
        result = self._ensure_consistent_schema(result)

        # Step 6: Apply specific discount logic
        discount_mask = result.select(
            pl.col("resource_id").is_in(list(resources_to_discount))
        ).to_series()
        
        if discount_mask.any():
            result = self._apply_specific_discount(result, discount_mask)
            
            self.logger.info(
                f"Applied {self.__class__.__name__} discounts",
                eligible_resources=len(eligible_resources),
                discounted_resources=discount_count,
                discount_percentage=self.discount_percentage
            )

        return result, newly_discounted

    def _ensure_consistent_schema(self, data: pl.DataFrame) -> pl.DataFrame:
        """Ensure consistent schema across all discount processors.
        
        Args:
            data: Input DataFrame
            
        Returns:
            DataFrame with consistent schema
        """
        result = data.clone()

        # Add standard columns if they don't exist
        standard_columns = {
            "usage_type": None,
            "operation": None,
            "line_item_type": "Usage",
            "description": None,
        }

        for col, default_value in standard_columns.items():
            if col not in result.columns:
                result = result.with_columns(pl.lit(default_value).alias(col))

        return result

    def _generate_discount_factor(self, size: int = 1) -> float:
        """Generate random discount factor within configured range.
        
        Args:
            size: Number of discount factors to generate
            
        Returns:
            Single discount factor or array of factors
        """
        factors = np.random.uniform(*self.discount_range, size=size)
        return factors[0] if size == 1 else factors