"""
Soft constraint evaluator - evaluate the soft constraint conditions of the itinerary
"""

from typing import Dict, Any, Tuple, List
import json

from utils import poi_analyzer
from .base_evaluator import BaseEvaluator
from utils.time_utils import TimeUtils
from utils.entity_utils import EntityUtils
from utils.poi_analyzer import POIAnalyzer
from utils.time_utils import _calculate_transport_hours
from utils.llms import Gemini, EmptyLLM, AbstractLLM
from utils.soft_constraint_prompts import get_classic_attractions_prompt, get_itinerary_diversity_prompt, parse_evaluation_classic_response, parse_evaluation_diverse_response

import math


class SoftConstraintEvaluator(BaseEvaluator):
    # Define the weights of various types of violations

    """Soft Constraint Evaluator Class"""
    
    def __init__(self, poi_analyzer: POIAnalyzer = None, enable_LLM: bool = False, llm: AbstractLLM = None, ):
        """
        Initialize Soft Constraint Evaluator
        
        Args:
            poi_analyzer: POI Analyzer Instance, create new instance if None
            enable_LLM: Whether to enable LLM scoring, default is True
        """
        super().__init__("SoftConstraintEvaluator")
        self.time_utils = TimeUtils()
        self.entity_utils = EntityUtils()
        self.poi_analyzer = poi_analyzer if poi_analyzer is not None else POIAnalyzer()
        self.enable_LLM = enable_LLM
        
        # Initialize LLM
        if self.enable_LLM:
            try:
                self.llm = llm if llm is not None else Gemini()
            except Exception as e:
                print(f"Warning: Failed to initialize Gemini LLM, falling back to EmptyLLM: {e}")
                self.llm = EmptyLLM()
        else:
            self.llm = EmptyLLM()


    def evaluate(self, data: Dict[str, Any]) -> Tuple[float, Dict[str, Any]]:
        """
        Evaluate Soft Constraint Conditions
        
        Args:
            data: Dictionary containing the following keys:
                - all_content: Content Data
                - itinerary: Itinerary Data
                - poi_dict: POI Dictionary
                - prompt_str: Prompt String (including user preferences)
                
        Returns:
            Tuple[float, Dict]: (Soft Constraint Score, Evaluation Details)
        """
        itinerary = data.get("itinerary", {})
        poi_dict = data.get("poi_dict", {})
        self.poi_dict = poi_dict
        prompt_str = data.get("prompt_str", "")
        
        violations = []
        scores = {}
        
        # 1. Check Schedule Density
        scores["density"], density_violations = self._check_schedule_density(itinerary, prompt_str)
        violations += density_violations
        
        # 2. Check Hotel Consistency
        scores["hotel"], hotel_violations = self._check_hotel_consistency(itinerary)
        violations += hotel_violations
        
        # 3. Check Daytime Utilization
        scores["utilization"], utilization_violations = self._check_daytime_utilization(itinerary)
        violations += utilization_violations
        
        # 4. Check Unique Attractions
        scores["unique"], unique_violations = self._check_unique_attractions(itinerary)
        violations += unique_violations
        
        # 5. Check Location Clustering
        scores["clustering"], clustering_violations = self._check_location_clustering(itinerary, poi_dict)
        violations += clustering_violations

        if self.enable_LLM:        
            self.SCORE_WEIGHTS = {
                "density": 0.7,
                "hotel": 0.5,
                "utilization": 0.4,
                "unique": 0.2,
                "clustering": 0.7,
                "classic_attractions": 0.1,  # New: Classic Attractions Coverage
                "diversity": 0.2             # New: Itinerary Diversity
            }

            # 6. Check Classic Attractions Coverage (based on LLM)
            scores["classic_attractions"], classic_violations = self._check_classic_attractions_coverage(itinerary, poi_dict)
            violations += classic_violations
            
            # 7. Check Itinerary Diversity (based on LLM)
            scores["diversity"], diversity_violations = self._check_itinerary_diversity(itinerary, poi_dict)
            violations += diversity_violations
        else:
            self.SCORE_WEIGHTS = {
                "density": 0.7,
                "hotel": 0.5,
                "utilization": 0.4,
                "unique": 0.2,
                "clustering": 0.7,
            }
        
        defined_weights, defined_scores = set(self.SCORE_WEIGHTS.keys()), set(scores.keys())
        if defined_weights != defined_scores:
            raise ValueError(
                f"Fatal error: Scores/Weights key mismatch. "
                f"Missing weights: {list(defined_scores - defined_weights)}. "
                f"Missing scores: {list(defined_weights - defined_scores)}."
            )

        total_score = sum(scores[k] * self.SCORE_WEIGHTS[k] for k in self.SCORE_WEIGHTS)
        weight_sum = sum(self.SCORE_WEIGHTS[k] for k in self.SCORE_WEIGHTS)
        total_score = total_score/weight_sum
        total_score = min(1.0, total_score)
        
        details = {
            "violations": violations,
            "scores": scores,
            "total_violations": len(violations),
            "density_violations": density_violations,
            "hotel_violations": hotel_violations,
            "utilization_violations": utilization_violations,
            "unique_violations": unique_violations,
            "clustering_violations": clustering_violations,
            "classic_attractions_violations": classic_violations if self.enable_LLM else [],
            "diversity_violations": diversity_violations if self.enable_LLM else [],
            "enable_LLM": self.enable_LLM
        }
        
        return total_score, details
    
    def _scale_rating_to_score(self, rating: int) -> float:
        """
        Convert 1-5 rating to 0-1 score
        
        Args:
            rating: 1-5 integer rating
            
        Returns:
            float: Score in the range of 0-1
        """
        # Ensure rating is within valid range
        rating = max(1, min(5, rating))
        # Map the range of 1-5 to 0-1
        return (rating - 1) / 4.0
    
    def _check_classic_attractions_coverage(self, itinerary: Dict, poi_dict: Dict) -> Tuple[float, List[str]]:
        """
        Check if the itinerary covers classic attractions (based on LLM judgment)
        
        Args:
            itinerary: Itinerary Data
            poi_dict: POI Dictionary
            
        Returns:
            Tuple[float, List[str]]: (Score, Violation List)
        """
        violations = []
        
        if not self.enable_LLM:
            # If LLM is not enabled, return default score
            return 1.0, []
        
        # Get destination city information
        locale = poi_dict.get("locale", "en-US")
        destination = poi_dict.get("arrive", "")
        
        daily_activities = {}
        for day_info in itinerary.get("dayInfos", []):
            day_id = day_info.get("day", "")
            activities = []
            for schedule in day_info.get("scheduleDetail", []):
                for detail in schedule.get("detailList", []):
                    if detail.get("type") == "poi":
                        activity_name = detail.get("name", "")
                        if activity_name:
                            activities.append(activity_name)
            if activities:
                daily_activities[f"Day {day_id}"] = activities
        
        if not daily_activities:
            violations.append("diversity_violation: No activities found in itinerary")
            return 0.0, violations

        # Use prompt template to build LLM prompt
        prompt = get_classic_attractions_prompt(destination, daily_activities, locale)
        
        try:
            # Call LLM
            messages = [{"role": "user", "content": prompt}]
            response = self.llm(messages, one_line=False, json_mode=False)

            # Parse response
            evaluation_result = parse_evaluation_classic_response(response)
            rating = int(evaluation_result.get("score", 3))  # Default is 3 (average)
            missing_attractions = evaluation_result.get("missing_attractions", [])
            explanation = evaluation_result.get("explanation", "")
            
            # Convert 1-5 rating to 0-1 score
            score = self._scale_rating_to_score(rating)
            
            # If there are missing important attractions, add violation information
            violations.append(f"classic_attractions_violation: Missing important attractions: {str(missing_attractions)}. Rating: {rating}/5. {explanation}")
            
            return score, violations
            
        except Exception as e:
            print(f"Error in classic attractions evaluation: {e}")
            # If there is an error, return a medium score
            violations.append(f"classic_attractions_violation: LLM evaluation failed: {str(e)}")
            return 0.5, violations
    
    def _check_itinerary_diversity(self, itinerary: Dict, poi_dict: Dict) -> Tuple[float, List[str]]:
        """
        Check if the itinerary is rich and diverse, avoiding homogeneity (based on LLM judgment)
        
        Args:
            itinerary: Itinerary Data
            poi_dict: POI Dictionary
            
        Returns:
            Tuple[float, List[str]]: (Score, Violation List)
        """
        violations = []
        
        if not self.enable_LLM:
            # If LLM is not enabled, return default score
            return 1.0, []
        
        # Collect all activity information in the itinerary
        number_activitis = 0
        number_days = len(itinerary.get("dayInfos", []))
        daily_activities = {}
        for day_info in itinerary.get("dayInfos", []):
            day_id = day_info.get("day", "")
            activities = []
            for schedule in day_info.get("scheduleDetail", []):
                for detail in schedule.get("detailList", []):
                    if detail.get("type") == "poi":
                        activity_name = detail.get("name", "")
                        number_activitis += 1
                        if activity_name:
                            activities.append(activity_name)
            if activities:
                daily_activities[f"Day {day_id}"] = activities
        number_ratio = min( number_activitis/(number_days*5), 1)
        if not daily_activities:
            violations.append("diversity_violation: No activities found in itinerary")
            return 0.0, violations
        
        locale = poi_dict.get("locale", "en-US")
        
        # Use prompt template to build LLM prompt
        prompt = get_itinerary_diversity_prompt(daily_activities, locale)
        
        try:
            # Call LLM
            messages = [{"role": "user", "content": prompt}]
            response = self.llm(messages, one_line=False, json_mode=False)
            evaluation_result = parse_evaluation_diverse_response(response)
            
            # Parse response
            rating = int(evaluation_result.get("score", 3))  # Default is 3 (average)
            diversity_issues = evaluation_result.get("diversity_issues", [])
            explanation = evaluation_result.get("explanation", "")
            
            # Convert 1-5 rating to 0-1 score
            score = self._scale_rating_to_score(rating)
            
            # If there are diversity issues, add violation information
            violations.append(f"diversity_violation: Diversity issues identified: {str(diversity_issues)}. Rating: {rating}/5. {explanation}")
            
            return (score + number_ratio)/2, violations
            
        except Exception as e:
            print(f"Error in diversity evaluation: {e}")
            # If there is an error, return a medium score
            violations.append(f"diversity_violation: LLM evaluation failed: {str(e)}")
            return 0.5, violations

    ## linear
    def _check_schedule_density(self, itinerary: Dict, prompt_str: str) -> Tuple[float, List[str]]:
        """Check Schedule Density (moderate-like, not user-dependent)"""
        violations = []
        days = itinerary.get("dayInfos", [])
        num_days = len(days)
        total_penalty = 0.0

        def get_penalty(num_activities: int, min_total_hours: float, max_total_hours: float, ind_day: int, transportation_time: str) -> (float, list):
            violated = []
            if min_total_hours < 4 and num_activities < 3:
                violated.append(
                    f"min_total_hours ({min_total_hours:.1f} < 4 ) and num_activities ({num_activities}) < 3")
            if ind_day == num_days-1:
                if transportation_time.lower() in ["morning", "上午", "afternoon", "下午", "evening", "晚上"]:
                    if max_total_hours > 10 and num_activities > 1:
                        violated.append(f"max_total_hours ({max_total_hours:.1f} > 10 ) and num_activities ({num_activities}) > 1")
                if transportation_time.lower() in ["morning", "上午"]:
                    if num_activities> 1 and min_total_hours > 5:
                        violated.append(f"min_hours ({min_total_hours:.1f} > 5)")
                elif transportation_time.lower() in ["afternoon", "下午"]:
                    if max_total_hours < 1:
                        violated.append(f"max_hours ({max_total_hours:.1f} < 1)")
                    if num_activities> 1 and min_total_hours > 14:
                        violated.append(f"min_hours ({min_total_hours:.1f} > 14)")
                else:
                    if max_total_hours < 3:
                        violated.append(f"max_hours ({max_total_hours:.1f} < 3)")
                    if num_activities> 1 and min_total_hours > 18:
                        violated.append(f"min_hours ({min_total_hours:.1f} > 18)")
            elif ind_day == 0:
                if transportation_time.lower() in ["morning", "上午", "afternoon", "下午", "evening", "晚上"]:
                    if max_total_hours > 10 and num_activities > 1:
                        violated.append(f"max_total_hours ({max_total_hours:.1f} > 10 ) and num_activities ({num_activities}) > 1")
                if transportation_time.lower() in ["evening", "晚上"]:
                    if num_activities> 1 and min_total_hours > 5:
                        violated.append(f"min_hours ({min_total_hours:.1f} > 5)")
                elif transportation_time.lower() in ["afternoon", "下午"]:
                    if max_total_hours < 1:
                        violated.append(f"max_hours ({max_total_hours:.1f} < 1)")
                    if num_activities> 1 and min_total_hours > 14:
                        violated.append(f"min_hours ({min_total_hours:.1f} > 14)")
                else:
                    if max_total_hours < 3:
                        violated.append(f"max_hours ({max_total_hours:.1f} < 3)")
                    if num_activities> 1 and min_total_hours > 18:
                        violated.append(f"min_hours ({min_total_hours:.1f} > 18)")
            else:
                if max_total_hours < 3:
                    violated.append(f"max_hours ({max_total_hours:.1f} < 3)")
                if num_activities> 1 and min_total_hours > 18:
                    violated.append(f"min_hours ({min_total_hours:.1f} > 18)")

            score = 1 if len(violated) > 0 else 0
            return score, violated

        def get_activities(day: Dict) -> List[Dict]:
            return [
                detail
                for schedule in day.get("scheduleDetail", [])
                for detail in schedule.get("detailList", [])
                if detail.get("type") == "poi"
            ]

        def get_transportations(day: Dict) -> List[Dict]:
            return [
                detail
                for schedule in day.get("scheduleDetail", [])
                for detail in schedule.get("detailList", [])
                if detail.get("type") == "transportation"
            ]

        for ind_day, day in enumerate(days):
            activities = get_activities(day)
            transportations = get_transportations(day)
            num_activities = len(list(set([a["name"] for a in activities])))
            # num_transportations = len(transportations)
            min_hours_total, max_hours_total = 0, 0
            transportation_time = ""
            for schedule in day.get("scheduleDetail", []):
                for detail in schedule.get("detailList", []):
                    if detail.get("type") == "transportation":
                        transportation_time = schedule['period']
            previous_poi = ""
            for activity in activities:
                activity_id = str(activity.get("id", ""))
                if activity_id != "" and activity_id != previous_poi:
                    activity_id = int(activity_id)
                    previous_poi = activity_id
                    api_info = self.poi_analyzer.read_poi_api_info(activity_id, self.poi_dict['locale'])
                    poi_info_simple = api_info[0]
                    try:
                        min_hours, max_hours = self.poi_analyzer.calculate_poi_hours(poi_info_simple)
                    except Exception as e:
                        print("error in calculate_poi_hours", str(e))
                        min_hours, max_hours = 0, 0
                    min_hours_total += min_hours
                    max_hours_total += max_hours
            for transportation in transportations:
                if transportation.get("id", "") != "":
                    transportation_info = self.poi_analyzer.get_transportation_info(transportation.get("id", ""))
                    if transportation_info != {}:
                        trans_time = _calculate_transport_hours(transportation_info.get('depature_time', ''), transportation_info.get('flight_time',''))
                        min_hours_total += trans_time
                        max_hours_total += trans_time
            penalty, violated = get_penalty(num_activities, min_hours_total, max_hours_total, ind_day, transportation_time)
            total_penalty += penalty

            if penalty > 0.0:
                violations.append(
                    f"density_violation: On day {day.get('day', '?')}, values: activities={num_activities}, min_hours={min_hours_total:.1f}, max_hours={max_hours_total:.1f}. Violated: {', '.join(violated)} (density threshold)"
                )

        score = max(0.0, 1.0 - total_penalty/max(len(days),1))
        return score, violations
    
    ## linear
    def _check_hotel_consistency(self, itinerary: Dict) -> Tuple[float, List[str]]:
        """Check Hotel Consistency - Penalize hotel switches within the same city"""

        def get_last_hotel(day: Dict) -> Dict | None:
            return next(
                (detail for schedule in reversed(day["scheduleDetail"])
                for detail in reversed(schedule["detailList"])
                if detail["type"] == "hotel"),
                None
            )

        def get_hotel_distance(current_hotel_id, hotel_id):
            distance = 200
            try:
                current_hotel_info = self.poi_analyzer.get_hotel_info(current_hotel_id, self.poi_dict['locale'])
                pre_hotel_info = self.poi_analyzer.get_hotel_info(hotel_id, self.poi_dict['locale'])
                pre_hotel_lat, pre_hotel_lon = pre_hotel_info['lat'], pre_hotel_info['lon']
                cur_hotel_lat, cur_hotel_lon = current_hotel_info['lat'], current_hotel_info['lon']
                distance = self.poi_analyzer.calculate_distance(pre_hotel_lat, pre_hotel_lon, cur_hotel_lat, cur_hotel_lon)
            except Exception as e:
                print("error in get_hotel_distance", str(e))
            return distance

        def get_hotel_switches(hotels: List[Tuple[int, str, str]]) -> List[Tuple[int, str, str, str]]:
            """
            Detect hotel switches within the same city
            
            Args:
                hotels: List of hotels sorted by time, each element is (day_id, hotel_id, hotel_city)
                
            Returns:
                List[Tuple[int, str, str, str]]: List of switches, each element is (day_id, from_hotel_id, to_hotel_id, city)
            """
            switches = []
            current_hotel_id = None
            current_city = None
            
            for day_id, hotel_id, hotel_city in hotels:
                # Only record hotel switches within the same city
                if current_hotel_id and current_city:
                    same_hotel = hotel_id == current_hotel_id
                    same_city = hotel_city == current_city
                    distance = get_hotel_distance(current_hotel_id, hotel_id)
                    if same_city and not same_hotel and distance < 60:
                        switches.append((day_id, current_hotel_id, hotel_id, hotel_city, distance))

                current_hotel_id = hotel_id
                current_city = hotel_city
            
            return switches

        violations = []
        
        hotels = []
        
        for day_info in itinerary.get("dayInfos", []):
            day_id = day_info["day"]
            if hotel := get_last_hotel(day_info):
                hotel_id = hotel.get("id", "")
                hotel_info = self.poi_analyzer.get_hotel_info(hotel_id, self.poi_dict["locale"])
                hotel_city_id = hotel_info["cityId"]
                hotels.append((day_id, hotel_id, hotel_city_id))

        # Check hotel switches within the same city
        hotel_switches = get_hotel_switches(hotels)
        
        for (day_id, from_hotel, to_hotel, city, distance) in hotel_switches:
            violations.append(f"hotel_violation: Same-city, nearby hotel change on Day {day_id}: {from_hotel} → {to_hotel}, distance: {distance}km")
        
        total_penalty = len(hotel_switches) / max(len(hotels), 1)
        total_score = max(0.0, 1.0 - total_penalty)
        
        return total_score, violations
    
    
    ## linear
    def _check_daytime_utilization(self, itinerary: Dict) -> Tuple[float, List[str]]:
        """Check Daytime Utilization - Only check two key standards"""
        violations = []
        total_penalty = 0.0
        
        def get_period_activities(day: Dict) -> Dict[str, List[Dict]]:
            """Get POI activities for each time period"""
            period_activities = {"Morning": [], "Afternoon": [], "Evening": []}
            
            for schedule in day.get("scheduleDetail", []):
                period = schedule.get("period", "")
                activities = [
                    detail for detail in schedule.get("detailList", [])
                    if detail.get("type") == "poi"
                ]
                if period in period_activities:
                    period_activities[period].extend(activities)
            
            return period_activities
        
        def has_evening_transport(day: Dict) -> bool:
            """Check if there is evening transportation"""
            for schedule in day.get("scheduleDetail", []):
                if schedule.get("period") == "Evening":
                    transport_details = [
                        detail for detail in schedule.get("detailList", [])
                        if detail.get("type") == "transportation"
                    ]
                    if transport_details:
                        return True
            return False

        dayInfos = itinerary.get("dayInfos", [])
        for day_info in dayInfos:
            day_id = day_info.get("day", "?")
            period_activities = get_period_activities(day_info)
            
            morning_activities = len(period_activities["Morning"])
            afternoon_activities = len(period_activities["Afternoon"])
            evening_activities = len(period_activities["Evening"])
            
            # 1. Check if there are activities only in the evening, no activities in other time periods
            if evening_activities > 0 and morning_activities == 0 and afternoon_activities == 0:
                violations.append(f"utilization_violation: Day {day_id} has activities only in evening, no daytime activities")
                total_penalty += 1
            
            # 2. Check if there is evening transportation but no morning or afternoon activities
            if has_evening_transport(day_info) and morning_activities == 0 and afternoon_activities == 0:
                violations.append(f"utilization_violation: Day {day_id} has evening transport but no morning or afternoon activities")
                total_penalty += 1
        
        score = max(0.0, 1.0 - total_penalty/max(len(dayInfos), 1))
        return score, violations
    
    ## lienar - number of repeated attractions
    ## exponential - number of times each attraction is repeated
    def _check_unique_attractions(self, itinerary: Dict) -> Tuple[float, List[str]]:
        """Check Unique Attractions"""
        def get_activities(day: Dict) -> List[Dict]:
            return [
                detail
                for schedule in day["scheduleDetail"]
                for detail in schedule["detailList"]
                if detail["type"] == "poi"
            ]

        violations = []
        all_activities = [
            (day["day"], activity)
            for day in itinerary.get("dayInfos", [])
            for activity in get_activities(day)
        ]
        
        # Check repeated attractions (excluding consecutive repeats)
        visited_attractions_times = {}
        visited_attractions_index = {}
        
        for i, [day_id, activity] in enumerate(all_activities):
            activity_id = activity.get("id", "")

            last_index = visited_attractions_index.get(activity_id, i)
            if last_index == i: # New activity encountered
                visited_attractions_times[activity_id] = 1
            
            if i - last_index > 1:  # Not consecutive
                visited_attractions_times[activity_id] = visited_attractions_times.get(activity_id, 0) + 1
                previous_day_id = all_activities[last_index][0]
                violations.append(f"unique_violation: Duplicate activity {activity['name']} on day {previous_day_id} and {day_id}")
 
            visited_attractions_index[activity_id] = i
        
        # Filter for duplicated attractions (times > 1)
        duplicated_attractions = { k: v for k, v in visited_attractions_times.items() if v > 1 }
        
        total_repeated_attractions = len(duplicated_attractions)
        linear_penalty = total_repeated_attractions/ max(len(all_activities), 1)
        exponential_penalty = sum((times - 1) ** 2 * 0.05 for times in duplicated_attractions.values()) / max(len(all_activities), 1)
        total_penalty = linear_penalty + exponential_penalty

        score = max(0.0, 1.0 - total_penalty)
        
        return score, violations
    
    def _check_location_clustering(self, itinerary: Dict, poi_dict: Dict) -> Tuple[float, List[str]]:
        """Check Location Clustering - Based on distance calculation"""
        violations = []
        total_penalty = 0.0
        
        def get_activities(day: Dict) -> List[Dict]:
            return [
                detail
                for schedule in day.get("scheduleDetail", [])
                for detail in schedule.get("detailList", [])
                if detail.get("type") == "poi"
            ]
        
        # Collect all activities and their coordinate information
        all_activities = [
            {
                "name": activity.get("name", ""),
                "day": day_info["day"],
                "lat": poi_info[0]["lat"],
                "lon": poi_info[0]["lon"],
                "order_ind": order_ind
            }
            for day_info in itinerary.get("dayInfos", [])
            for order_ind, activity in enumerate(get_activities(day_info))
            if (poi_info := self.poi_analyzer.read_poi_api_info(activity.get("id", ""), poi_dict["locale"]))[0] and
               "lat" in poi_info[0] and "lon" in poi_info[0]
        ]
        
        # Calculate distances between all activity pairs
        all_distances = []
        for i in range(len(all_activities)):
            for j in range(i + 1, len(all_activities)):
                activity1 = all_activities[i]
                activity2 = all_activities[j]
                
                distance = self.poi_analyzer.calculate_distance(
                    activity1["lat"], activity1["lon"],
                    activity2["lat"], activity2["lon"]
                )
                if distance < 1:
                    continue
                all_distances.append((distance, {
                    "activity1": {"name": activity1["name"], "day": activity1["day"], "order_ind": activity1["order_ind"]},
                    "activity2": {"name": activity2["name"], "day": activity2["day"], "order_ind": activity2["order_ind"]}
                }))
        
        # Sort by distance, find the farthest pair
        all_distances.sort(key=lambda x: x[0], reverse=True)
        
        # Only penalize the farthest distance of the top 20%
        if len(all_distances) <= 2:
            return 1.0, []

        num_to_penalize = max(1, int(len(all_distances) * 0.2))
        top_distances = all_distances[:num_to_penalize]
        
        # Apply exponential penalty: only penalize consecutive distances
        penalty_index = 0
        for i, (distance, dist_info) in enumerate(top_distances):
            # Check if the activities are on the same day
            activity1_day = dist_info["activity1"]["day"]
            activity2_day = dist_info["activity2"]["day"]
            # Check if the activities are consecutive
            activity1_order = dist_info["activity1"]["order_ind"]
            activity2_order = dist_info["activity2"]["order_ind"]
            # Only penalize distances within the same day
            if activity1_day == activity2_day:
                if abs(activity1_order - activity2_order) > 1:
                    continue
                if distance < 1:
                    continue
                penalty = 1 * (0.5 ** penalty_index)
                total_penalty += penalty
                penalty_index += 1
                
                violations.append(
                    f"clustering_violation: Day {activity1_day} - Distance {i+1} ({distance:.1f}km) "
                    f"between \"{dist_info['activity1']['name']}\" and \"{dist_info['activity2']['name']}\" - "
                    f"penalty: {penalty:.3f}"
                )
        
        score = max(0.0, 1.0 - total_penalty/max(len(all_activities),1))
        return score, violations
    
    def _parse_duration(self, duration_str: str) -> float:
        """Parse duration string"""
        import re
        
        # Match hours
        hour_match = re.search(r'(\d+(?:\.\d+)?)\s*hours?', duration_str.lower())
        if hour_match:
            return float(hour_match.group(1))
        
        # Match minutes
        minute_match = re.search(r'(\d+)\s*minutes?', duration_str.lower())
        if minute_match:
            return float(minute_match.group(1)) / 60
        
        return 0.0