"""
M:N Resource-to-Workload Mapping Service for K8s allocation.

Implements deterministic mapping where:
- Each resource maps to 2-5 workloads (M:N relationship)
- Each workload gets at least 2 resources (redundancy guarantee)
- Uses enhanced seeding for cross-month consistency
- Dirichlet distribution for proportional cost splitting
"""

import hashlib
import numpy as np
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, List, Set, Optional, Any

import polars as pl
from numpy.random import Generator

from src.log_context import get_logger
from ..config import K8sConfig


@dataclass
class K8sTopology:
    """Represents the complete K8s topology structure."""
    clusters: List[str]
    namespaces: Dict[str, List[str]]  # cluster_id -> namespaces
    workloads: Dict[str, List[str]]   # namespace -> workloads
    resource_mappings: Dict[str, List[str]]  # resource_id -> list of workloads
    cluster_environments: Dict[str, str]  # cluster_id -> environment (prod/prd/dev/etc)


class DeterministicIdGenerator:
    """
    Generates deterministic K8s component IDs based on seeds.
    
    Ensures consistent topology generation across months by using
    the same seed for the same dataset and resource combination.
    """
    
    def __init__(self, default_namespaces: Set[str]):
        self.default_namespaces = default_namespaces
        
        # Predefined name components for realistic naming
        self.environments = 3 * ["prod"] + 3 * ["prd"] + ["dev", "develop", "staging", "test", "sandbox"]
        self.regions = ["us-east", "us-west", "eu-west", "ap-south", "sa-east"]
        self.purposes = ["app", "data", "ml", "etl", "web", "api"]
        self.team_prefixes = ["team", "dept", "group", "org", "unit"]
        self.workload_types = ["service", "deployment", "job", "daemon", "stateful"]
        self.component_types = ["frontend", "backend", "worker", "scheduler", "processor", "cache"]
        
        # Cache for hash computations
        self._hash_cache = {}
        
        self.logger = get_logger(__name__)
    
    def _hash_based_selection(self, items: List[str], seed: str) -> str:
        """Select an item from a list based on hash value for deterministic selection."""
        if seed not in self._hash_cache:
            self._hash_cache[seed] = hashlib.md5(seed.encode()).hexdigest()
        index = int(self._hash_cache[seed], 16) % len(items)
        return items[index]
    
    def generate_id(self, resource_id: str, prefix: str, seed: int) -> str:
        """
        Generate deterministic ID based on resource_id and seed.
        
        Args:
            resource_id: Source resource ID for deterministic generation
            prefix: Type of ID to generate (cluster, namespace, workload)
            seed: Numeric seed for variation
            
        Returns:
            Deterministic ID string
        """
        # Include provider in seed for provider-specific cluster names
        provider = getattr(self, '_current_provider', '')
        input_str = f"{resource_id}:{prefix}:{seed}:{provider}"
        hash_obj = hashlib.md5(input_str.encode())
        hash_value = hash_obj.hexdigest()[:8]
        
        if prefix == "cluster":
            env = self._hash_based_selection(self.environments, f"{input_str}:env")
            region = self._hash_based_selection(self.regions, f"{input_str}:region")
            purpose = self._hash_based_selection(self.purposes, f"{input_str}:purpose")
            return f"{env}-{region}-{purpose}-{hash_value[:4]}"
        
        elif prefix == "namespace":
            if hash_value[:2] in ["00", "01", "02", "03"]:
                # System or platform namespace
                return self._hash_based_selection(list(self.default_namespaces), input_str)
            else:
                # Team or project namespace
                team_prefix = self._hash_based_selection(self.team_prefixes, f"{input_str}:team")
                return f"{team_prefix}-{hash_value[:6]}"
        
        elif prefix == "workload":
            workload_type = self._hash_based_selection(self.workload_types, f"{input_str}:type")
            component = self._hash_based_selection(self.component_types, f"{input_str}:component")
            return f"{workload_type}-{component}-{hash_value[:4]}"
        
        return f"{prefix}-{hash_value}"
    
    def generate_topology(
        self, 
        base_seed: str, 
        clusters_count: int, 
        namespaces_per_cluster: int, 
        workloads_per_namespace: int
    ) -> K8sTopology:
        """
        Generate a complete K8s topology with deterministic structure.
        
        Args:
            base_seed: Base seed derived from dataset + resource IDs
            clusters_count: Number of clusters to generate
            namespaces_per_cluster: Custom namespaces per cluster (beyond defaults)
            workloads_per_namespace: Workloads per namespace
            
        Returns:
            Complete K8s topology structure
        """
        clusters = []
        namespaces = {}
        workloads = {}
        cluster_environments = {}
        
        self.logger.debug(
            "Generating K8s topology",
            base_seed=base_seed,
            clusters_count=clusters_count,
            namespaces_per_cluster=namespaces_per_cluster,
            workloads_per_namespace=workloads_per_namespace
        )
        
        # Generate cluster IDs and track their environments
        for i in range(clusters_count):
            cluster_id = self.generate_id(base_seed, "cluster", i)
            clusters.append(cluster_id)
            namespaces[cluster_id] = []
            
            # Extract environment from cluster name (format: "env-region-purpose-hash")
            env = cluster_id.split('-')[0]
            cluster_environments[cluster_id] = env
            
            # Add default namespaces
            for ns in self.default_namespaces:
                namespaces[cluster_id].append(ns)
                # Initialize workloads list only if it doesn't exist (prevents overwriting)
                if ns not in workloads:
                    workloads[ns] = []
                
                # Add workloads to default namespace
                base_workload_seed = i * len(self.default_namespaces) + hash(ns) % 1000
                for j in range(workloads_per_namespace):
                    workload_id = self.generate_id(base_seed, "workload", base_workload_seed + j)
                    workloads[ns].append(workload_id)
            
            # Add custom namespaces
            base_namespace_seed = i * namespaces_per_cluster
            for j in range(namespaces_per_cluster):
                ns = self.generate_id(base_seed, "namespace", base_namespace_seed + j)
                namespaces[cluster_id].append(ns)
                workloads[ns] = []
                
                # Add workloads to custom namespace
                base_workload_seed = base_namespace_seed * workloads_per_namespace + j * 100
                for k in range(workloads_per_namespace):
                    workload_id = self.generate_id(base_seed, "workload", base_workload_seed + k)
                    workloads[ns].append(workload_id)
            
            # Add Idle namespace and workload
            idle_namespace = "__UNALLOCATED__"
            idle_workload = "__UNALLOCATED__"
            namespaces[cluster_id].append(idle_namespace)
            workloads[idle_namespace] = [idle_workload]
        
        # Initialize empty resource mappings (to be populated later)
        resource_mappings = {}
        
        topology = K8sTopology(clusters, namespaces, workloads, resource_mappings, cluster_environments)
        
        self.logger.info(
            "Generated K8s topology",
            clusters=len(clusters),
            total_namespaces=sum(len(ns_list) for ns_list in namespaces.values()),
            total_workloads=sum(len(wl_list) for wl_list in workloads.values())
        )
        
        return topology


class ResourceToWorkloadMapper:
    """
    Handles M:N resource-to-workload mapping with deterministic assignment.
    
    Implements the core mapping logic where:
    - Each resource maps to 2-5 workloads (configurable)
    - Each workload gets at least 2 resources (redundancy guarantee)
    - Uses deterministic seeding for cross-month consistency
    """
    
    def __init__(self, config: K8sConfig):
        self.config = config
        self.logger = get_logger(__name__)
        
        # Enhanced seeding strategy for cross-month consistency
        self.base_seed = self._create_base_seed()
        self.rng = np.random.default_rng(self._create_numeric_seed())
        
        # Default namespaces for topology generation
        self.default_namespaces = {"default", "kube-system", "kube-node-lease", "kube-public"}
        
        # Initialize ID generator
        self.id_generator = DeterministicIdGenerator(self.default_namespaces)
        
        self.logger.info(
            "Initialized ResourceToWorkloadMapper",
            dataset_name=config.dataset_name,
            base_seed=self.base_seed[:10] + "...",
            min_resources_per_workload=config.min_resources_per_workload
        )
    
    def _create_base_seed(self) -> str:
        """
        Create enhanced base seed for deterministic topology generation.
        
        Uses dataset name to ensure consistent topology across months
        for the same dataset, but different topologies for different datasets.
        
        NOTE: This only creates the base topology seed. For resource sampling
        consistency, use _create_resource_sampling_seed() with resource fingerprint.
        """
        seed_input = f"dataset:{self.config.dataset_name}"
        return hashlib.md5(seed_input.encode()).hexdigest()
    
    def _create_resource_sampling_seed(self, eligible_resource_ids: List[str]) -> int:
        """
        Create deterministic seed for resource sampling that includes stable resource fingerprint.
        
        This ensures that the same set of eligible resources produces the same sampling
        results across months, fixing the cross-month consistency bug.
        
        Args:
            eligible_resource_ids: List of eligible resource IDs (must be pre-sorted)
            
        Returns:
            Numeric seed for resource sampling RNG
        """
        # Resource IDs are now pre-sorted in processor_service.py for consistency
        # No need to sort again - use as-is to maintain deterministic ordering
        sorted_resource_ids = eligible_resource_ids
        
        # Take a stable subset for fingerprint to handle very large resource sets
        # Use first 100 and last 100 resource IDs to create representative fingerprint
        if len(sorted_resource_ids) <= 200:
            fingerprint_resources = sorted_resource_ids
        else:
            fingerprint_resources = sorted_resource_ids[:100] + sorted_resource_ids[-100:]
        
        # Create fingerprint hash
        resource_fingerprint = hashlib.md5('|'.join(fingerprint_resources).encode()).hexdigest()
        
        # Combine dataset context with resource fingerprint
        seed_input = f"dataset:{self.config.dataset_name}:resources:{resource_fingerprint}"
        seed_hash = hashlib.md5(seed_input.encode()).hexdigest()
        
        return int(seed_hash[:8], 16) % (2**32)
    
    def _create_numeric_seed(self) -> int:
        """Create numeric seed for RNG initialization."""
        return int(self.base_seed[:8], 16) % (2**32)
    
    def calculate_resource_allocation(self, eligible_resource_ids: List[str]) -> tuple[List[str], int]:
        """
        Calculate optimal resource allocation for dual-constraint algorithm.
        
        Args:
            eligible_resource_ids: List of all eligible compute resource IDs
            
        Returns:
            Tuple of (sampled_resource_ids, workloads_per_namespace)
        """
        from ..config.models import (
            MINIMUM_RESOURCES_REQUIRED, 
            OPTIMAL_RESOURCES_LOW_DENSITY,
            OPTIMAL_RESOURCES_HIGH_DENSITY,
            DENSITY_THRESHOLD
        )
        
        available_resources = len(eligible_resource_ids)
        
        # Fail-fast check
        if available_resources < MINIMUM_RESOURCES_REQUIRED:
            raise ValueError(
                f"Insufficient resources for K8s allocation. "
                f"Available: {available_resources}, Required minimum: {MINIMUM_RESOURCES_REQUIRED}. "
                f"Please increase eligible compute resources or use different provider."
            )
        
        # Determine topology density and resource requirements
        if available_resources <= DENSITY_THRESHOLD:
            # Low density: 70 workloads, optimal 53 resources
            workloads_per_namespace = 2
            optimal_resources = OPTIMAL_RESOURCES_LOW_DENSITY
        else:
            # High density: 105 workloads, optimal 79 resources  
            workloads_per_namespace = 3
            optimal_resources = OPTIMAL_RESOURCES_HIGH_DENSITY
        
        # Use optimal count or all available resources (whichever is smaller)
        actual_resources = min(available_resources, optimal_resources)
        
        # Deterministic sampling using resource-aware seed for cross-month consistency
        if actual_resources >= len(eligible_resource_ids):
            # Use all resources
            sampled_resource_ids = eligible_resource_ids.copy()
            self.logger.debug("Using all available resources (no sampling needed)")
        else:
            # Sample deterministically using resource fingerprint seed
            resource_sampling_seed = self._create_resource_sampling_seed(eligible_resource_ids)
            sampling_rng = np.random.default_rng(resource_sampling_seed)
            
            sampled_indices = sampling_rng.choice(
                len(eligible_resource_ids), 
                size=actual_resources, 
                replace=False
            )
            sampled_resource_ids = [eligible_resource_ids[i] for i in sorted(sampled_indices)]
            
            self.logger.debug(
                "Applied deterministic resource sampling with enhanced seed",
                resource_sampling_seed=resource_sampling_seed,
                sampled_first_3=sampled_resource_ids[:3],
                total_sampled=len(sampled_resource_ids)
            )
        
        self.logger.info(
            "Calculated resource allocation",
            eligible_resources=len(eligible_resource_ids),
            optimal_resources=optimal_resources,
            actual_resources=actual_resources,
            sampled_resources=len(sampled_resource_ids),
            workloads_per_namespace=workloads_per_namespace,
            density_type="low" if workloads_per_namespace == 2 else "high"
        )
        
        return sampled_resource_ids, workloads_per_namespace

    def generate_topology(self, resource_ids: List[str], provider: str = "", workloads_per_namespace: int = 2) -> K8sTopology:
        """
        Generate K8s topology using dataset-based seeding with provider isolation.
        
        Args:
            resource_ids: List of resource IDs (unused for topology generation)
            provider: Cloud provider for provider-specific topology generation
            
        Returns:
            Complete K8s topology structure
        """
        # Set provider context for deterministic ID generation
        self.id_generator._current_provider = provider
        
        # Use base_seed + provider for topology generation to ensure provider isolation
        topology = self.id_generator.generate_topology(
            self.base_seed,  # ✅ Same seed every month = same topology
            self.config.clusters_count,  # Use config value (5)
            self.config.namespaces_per_cluster,  # Use config value (3) - additional namespaces beyond defaults
            workloads_per_namespace  # 2 or 3 based on resource density
        )
        
        return topology
    
    def validate_resource_availability(self, resource_ids: List[str]) -> Dict[str, Any]:
        """
        Validate that there are enough resources to satisfy workload minimum requirements.
        
        Args:
            resource_ids: List of unique resource IDs to be mapped
            
        Returns:
            Validation report with feasibility assessment
        """
        # Calculate expected workload counts based on topology
        total_workloads = (
            self.config.clusters_count * 
            (len(self.default_namespaces) + self.config.namespaces_per_cluster) *
            self.config.workloads_per_namespace
        )
        
        # Calculate minimum resources needed for all workloads
        min_resources_needed = total_workloads * self.config.min_resources_per_workload
        
        # Calculate how many resources we have
        available_resources = len(resource_ids)
        
        # Assess feasibility
        feasible = available_resources >= min_resources_needed
        
        validation_report = {
            "feasible": feasible,
            "total_workloads": total_workloads,
            "min_resources_needed": min_resources_needed,
            "available_resources": available_resources,
            "resource_deficit": max(0, min_resources_needed - available_resources),
            "resource_surplus": max(0, available_resources - min_resources_needed),
            "utilization_rate": min(1.0, min_resources_needed / available_resources) if available_resources > 0 else 0
        }
        
        self.logger.info(
            "Resource availability validation",
            **validation_report
        )
        
        if not feasible:
            self.logger.error(
                "Insufficient resources for workload minimum requirements",
                deficit=validation_report["resource_deficit"],
                need=min_resources_needed,
                have=available_resources
            )
        
        return validation_report

    def distribute_resources_dual_constraint(self, topology: K8sTopology, resource_ids: List[str]) -> Dict[str, List[str]]:
        """
        Dual-constraint algorithm: Each resource gets exactly 4 workloads (3 regular + 1 Idle).
        Natural balance ensures workloads get 2-5 resources through optimal resource count.
        
        Args:
            topology: K8s topology structure
            resource_ids: List of resource IDs to distribute
            
        Returns:
            Dictionary mapping resource_id -> list of workloads
        """
        # Get all regular workloads (excluding Idle)
        available_workloads = []
        for workloads in topology.workloads.values():
            available_workloads.extend([w for w in workloads if w != "__UNALLOCATED__"])
        
        if not available_workloads:
            raise ValueError("No workloads available in topology for resource distribution")
        
        # Sort for deterministic processing
        available_workloads = sorted(available_workloads)
        resource_mappings = {}
        workload_counts = {workload: 0 for workload in available_workloads}
        
        self.logger.info(
            "Starting dual-constraint resource allocation",
            total_workloads=len(available_workloads),
            available_resources=len(resource_ids),
            workloads_per_resource=self.config.workloads_per_resource,
            min_resources_per_workload=self.config.min_resources_per_workload,
            max_resources_per_workload=self.config.max_resources_per_workload
        )
        
        # Two-phase allocation to ensure minimum requirements
        total_slots = len(resource_ids) * 3  # 3 regular workloads per resource
        
        # Phase 1: Ensure every workload gets minimum 2 resources
        assignments_needed = len(available_workloads) * 2
        if total_slots < assignments_needed:
            self.logger.warning(
                "Insufficient total slots for minimum workload requirements",
                total_slots=total_slots,
                assignments_needed=assignments_needed,
                deficit=assignments_needed - total_slots
            )
        
        # Phase 2: Resource-first assignment with priority balancing
        for resource_index, resource_id in enumerate(resource_ids):
            selected_workloads = []
            
            # Select 3 regular workloads using strict priority system
            for slot in range(3):
                # Priority 1: Workloads with 0 resources (critical)
                zero_resources = [w for w in available_workloads if workload_counts[w] == 0]
                
                # Priority 2: Workloads with 1 resource (under minimum)
                one_resource = [w for w in available_workloads if workload_counts[w] == 1]
                
                # Priority 3: Workloads with 2-4 resources (can accept more)
                under_limit = [w for w in available_workloads if 2 <= workload_counts[w] < 5]
                
                # Priority 4: All workloads (fallback)
                all_available = available_workloads
                
                # Select from highest priority group with deterministic seeding
                if zero_resources:
                    candidates = zero_resources
                    priority = "zero"
                elif one_resource:
                    candidates = one_resource
                    priority = "one"
                elif under_limit:
                    candidates = under_limit
                    priority = "under_limit"
                else:
                    # Find least-loaded workloads
                    min_count = min(workload_counts.values())
                    candidates = [w for w in available_workloads if workload_counts[w] == min_count]
                    priority = "least_loaded"
                
                # Deterministic selection
                context = f"{resource_id}:{slot}:{priority}:{resource_index}"
                workload = self._deterministic_choice(candidates, context)
                selected_workloads.append(workload)
                workload_counts[workload] += 1
            
            # Always add __UNALLOCATED__ workload (4th workload)
            selected_workloads.append("__UNALLOCATED__")
            
            resource_mappings[resource_id] = selected_workloads
        
        # Build workload tracking for validation (exclude __UNALLOCATED__)
        workload_to_resources = defaultdict(set)
        for resource_id, workloads in resource_mappings.items():
            for workload in workloads:
                if workload != "__UNALLOCATED__":
                    workload_to_resources[workload].add(resource_id)
        
        # Validation: Check dual constraints
        self._validate_dual_constraints(resource_mappings, workload_to_resources)
        
        self.logger.info(
            "Dual-constraint allocation completed",
            total_resources=len(resource_mappings),
            total_workloads=len(workload_to_resources),
            avg_resources_per_workload=sum(len(res) for res in workload_to_resources.values()) / len(workload_to_resources),
            expansion_factor=4.0
        )
        
        return resource_mappings
    
    def _deterministic_choice(self, candidates: List[str], context: str) -> str:
        """Deterministically choose from candidates using context-based seeding."""
        if not candidates:
            raise ValueError("No candidates available for deterministic choice")
        
        # Create deterministic seed from base seed and context
        seed_input = f"{self.base_seed}:{context}"
        seed = int(hashlib.md5(seed_input.encode()).hexdigest()[:8], 16) % (2**32)
        rng = np.random.default_rng(seed)
        
        # Choose index deterministically
        index = rng.integers(0, len(candidates))
        return candidates[index]
    
    def _validate_dual_constraints(self, resource_mappings: Dict[str, List[str]], workload_to_resources: Dict[str, Set[str]]) -> None:
        """Validate both resource and workload constraints."""
        
        if not resource_mappings:
            self.logger.info("No resource assignments to validate")
            return
        
        # Resource constraint validation: Each resource should have exactly 4 workloads
        resource_violations = []
        for resource_id, workloads in resource_mappings.items():
            if len(workloads) != 4:
                resource_violations.append((resource_id, len(workloads)))
        
        # Workload constraint validation: Each workload should have 2-5 resources (excluding Idle)
        workload_violations = []
        for workload, resources in workload_to_resources.items():
            resource_count = len(resources)
            if resource_count < self.config.min_resources_per_workload:
                workload_violations.append((workload, resource_count, "below_minimum"))
            elif resource_count > self.config.max_resources_per_workload:
                workload_violations.append((workload, resource_count, "above_maximum"))
        
        # Report violations (should be impossible with dual-constraint algorithm)
        if resource_violations:
            error_summary = (
                f"❌ RESOURCE CONSTRAINT VIOLATIONS: {len(resource_violations)} resources don't have exactly 4 workloads:\\n" +
                "\\n".join(f"  • {resource_id}: {count} workloads (expected: 4)" 
                         for resource_id, count in resource_violations[:5])
            )
            self.logger.error("Resource constraint violations", violations=len(resource_violations))
            raise ValueError(error_summary)
        
        if workload_violations:
            error_summary = (
                f"❌ WORKLOAD CONSTRAINT VIOLATIONS: {len(workload_violations)} workloads outside 2-5 resource range:\\n" +
                "\\n".join(f"  • {workload}: {count} resources ({violation_type})" 
                         for workload, count, violation_type in workload_violations[:5])
            )
            self.logger.error("Workload constraint violations", violations=len(workload_violations))
            raise ValueError(error_summary)
        
        # Success metrics
        total_resources = len(resource_mappings)
        total_workloads = len(workload_to_resources)
        total_assignments = sum(len(workloads) for workloads in resource_mappings.values())
        avg_resources = sum(len(resources) for resources in workload_to_resources.values()) / total_workloads if total_workloads > 0 else 0
        
        self.logger.info(
            "Dual-constraint validation passed",
            total_resources=total_resources,
            total_workloads=total_workloads,
            total_assignments=total_assignments,
            avg_resources_per_workload=round(avg_resources, 2),
            expansion_factor=round(total_assignments / total_resources, 1) if total_resources > 0 else 0,
            min_resources_per_workload=self.config.min_resources_per_workload,
            max_resources_per_workload=self.config.max_resources_per_workload
        )
    
    def _validate_workload_constraints(self, workload_to_resources: Dict[str, Set[str]]) -> None:
        """Validate only workload constraints (simplified algorithm)."""
        
        if not workload_to_resources:
            self.logger.info("No workload assignments to validate")
            return
        
        insufficient_workloads = []
        excessive_workloads = []
        
        for workload, resources in workload_to_resources.items():
            resource_count = len(resources)
            if resource_count < self.config.min_resources_per_workload:
                insufficient_workloads.append((workload, resource_count))
            elif resource_count > self.config.max_resources_per_workload:
                excessive_workloads.append((workload, resource_count))
        
        # Report violations (but don't fail - workload-only algorithm should prevent these)
        if insufficient_workloads:
            self.logger.warning(
                "Workloads below minimum resource requirement",
                count=len(insufficient_workloads),
                min_required=self.config.min_resources_per_workload,
                examples=insufficient_workloads[:3]
            )
        
        if excessive_workloads:
            # This should be impossible with workload-only algorithm
            error_summary = (
                f"❌ ALGORITHM ERROR: {len(excessive_workloads)} workloads exceed maximum resources "
                f"(should be impossible with workload-only algorithm):\n" +
                "\n".join(f"  • {workload}: {count} resources (max: {self.config.max_resources_per_workload})" 
                         for workload, count in excessive_workloads[:5])
            )
            self.logger.error("Workload-only algorithm violation", excessive_count=len(excessive_workloads))
            raise ValueError(error_summary)
        
        # Success metrics
        total_workloads = len(workload_to_resources)
        total_resources = sum(len(resources) for resources in workload_to_resources.values())
        avg_resources = total_resources / total_workloads if total_workloads > 0 else 0
        perfect_workloads = total_workloads - len(insufficient_workloads) - len(excessive_workloads)
        
        self.logger.info(
            "Workload constraint validation completed",
            total_workloads=total_workloads,
            perfect_workloads=perfect_workloads,
            insufficient_workloads=len(insufficient_workloads),
            excessive_workloads=len(excessive_workloads),
            avg_resources_per_workload=round(avg_resources, 2),
            min_resources_per_workload=self.config.min_resources_per_workload,
            max_resources_per_workload=self.config.max_resources_per_workload
        )
    def split_costs_deterministically(self, resource_id: str, cost: float, workloads: List[str], 
                                    topology: Optional[K8sTopology] = None) -> Dict[str, float]:
        """
        Split cost among workloads using Dirichlet distribution with environment-based bonuses.
        
        Args:
            resource_id: Resource ID for deterministic seeding
            cost: Total cost to split
            workloads: List of workloads to split cost among
            topology: K8s topology for environment-based cost adjustments (optional for backward compatibility)
            
        Returns:
            Dictionary mapping workload -> allocated cost
        """
        if not workloads:
            return {}
        
        # Use deterministic RNG for cost splitting
        cost_rng = np.random.default_rng(
            int(hashlib.md5(f"{self.base_seed}:{resource_id}:cost".encode()).hexdigest()[:8], 16) % (2**32)
        )
        
        # Dirichlet distribution for proportional splitting
        base_weights = cost_rng.dirichlet(np.ones(len(workloads)))
        
        # Apply environment-based adjustments if topology is provided
        if topology and topology.cluster_environments:
            adjusted_weights = self._apply_environment_bonus(base_weights, workloads, topology)
        else:
            adjusted_weights = base_weights
            
        # Allocate costs with rounding
        cost_allocation = {}
        for workload, weight in zip(workloads, adjusted_weights):
            cost_allocation[workload] = round(cost * weight, 6)
        
        return cost_allocation
    
    def _apply_environment_bonus(self, base_weights: np.ndarray, workloads: List[str], 
                               topology: K8sTopology) -> np.ndarray:
        """
        Apply environment-based cost bonuses to workload weights.
        
        Production environments get 15% bonus, non-production get 15% penalty.
        Total cost is preserved through renormalization.
        
        Args:
            base_weights: Base Dirichlet weights
            workloads: List of workloads
            topology: K8s topology with cluster environments
            
        Returns:
            Adjusted weights that sum to 1.0
        """
        # Build reverse mappings for workload -> cluster lookup
        workload_to_namespace = {}
        namespace_to_cluster = {}
        
        for namespace, namespace_workloads in topology.workloads.items():
            for workload in namespace_workloads:
                workload_to_namespace[workload] = namespace
        
        for cluster, namespaces in topology.namespaces.items():
            for namespace in namespaces:
                namespace_to_cluster[namespace] = cluster
        
        # Apply environment multipliers
        adjusted_weights = []
        for i, workload in enumerate(workloads):
            # Get workload's cluster environment
            namespace = workload_to_namespace.get(workload, "unknown")
            cluster = namespace_to_cluster.get(namespace, "unknown")
            environment = topology.cluster_environments.get(cluster, "unknown")
            
            # Apply environment multiplier
            if environment in ["prod", "prd"]:
                # Production clusters get 15% bonus
                multiplier = 1.15
            else:
                # Non-production clusters get 15% penalty
                multiplier = 0.85
            
            adjusted_weights.append(base_weights[i] * multiplier)
        
        # Convert to numpy array and renormalize to preserve total cost
        adjusted_weights = np.array(adjusted_weights)
        adjusted_weights = adjusted_weights / adjusted_weights.sum()
        
        return adjusted_weights