from copy import deepcopy
import copy
import json
import os
import sys
import time
from typing import Dict, List
from json_repair import repair_json
from tqdm import tqdm
import pandas as pd
import numpy as np
import math


from agent.nesy_agent.nl2sl_translator import NL2SLTranslator
from agent.base import BaseAgent
from utils.poi_analyzer import POIAnalyzer
from agent.nesy_agent.nesy_utils.hard_constraint import evaluate_constraints_py, func_commonsense_constraints
from utils.time_utils import TimeUtils
from agent.nesy_agent.nesy_logger import NesyLogger



period_mapping = {
    "Morning": 1,
    "Afternoon": 2,
    "Evening":3,
    "上午":1,
    "下午":2,
    "晚上":3
}

class TimeOutError(Exception):
    def __init__(self, message="Searching TIME OUT !!!"):
        self.message = message
        super().__init__(self.message)


class NesyAgent(BaseAgent):
    """NeSy Agent for trip planning"""
    
    def __init__(self, env=None, backbone_llm=None, cache_dir=None, debug=False, **kwargs):
        super().__init__(name="LLMNeSy", **kwargs)

        self.env = env
        self.backbone_llm = backbone_llm
        self.cache_dir = cache_dir
        self.debug = debug
        self.max_steps = kwargs.get('max_steps', 0)
        self.debug = kwargs.get("debug", False)
        self.memory = {}
        self.TIME_CUT = 60 * 5
        self.least_plan_schema, self.least_plan_comm = None, None
        self.method = kwargs.get("method", "NeSy")  # agent
        self.search_width = kwargs.get("search_width", None)
        self.preference_search = False
        self.prompt_upd = True
        self.poi_analyzer = POIAnalyzer()  # POI analyzer
        self.unique_id = 1000
        
        # Initialize logger
        log_dir = kwargs.get("log_dir", "cache/LLMNeSy_default")
        self.logger = NesyLogger(log_dir, self.debug)
        self.logger.info(f"Initialized NeSy Agent with method: {self.method}")


    def reset(self):
            pass
        
    def run(self, query, load_cache=True, oralce_translation=False, preference_search=False):
        """
        Run the NeSy agent
        
        Args:
            query: The query data containing travel requirements
            load_cache: Whether to load from cache
            oralce_translation: Whether to use oracle translation
            preference_search: Whether to enable preference search
            
        Returns:
            tuple: (success, plan)
        """
        start_time = time.time()
        self.logger.info("Starting NeSy Agent execution")
        
        try:
            # Extract query information
            user_query = query.get('userQuery', '')
            days = query.get('day', '')
            departure = query.get('departure', '')
            arrive = query.get('arrive', '')
            
            self.logger.info(f"Processing query: {departure} -> {arrive}, {days} days")
            self.logger.debug(f"User query: {user_query}")
            
            # translate_nl2sl
            self.logger.step("Starting NL2SL translation")
            nl2sl_translator = NL2SLTranslator(self.backbone_llm)
            query = nl2sl_translator.translate_nl2sl(query)    
            self.logger.info(f"NL2SL translation query keys: {query.keys()}")
            self.logger.info(f"NL2SL translation successful: {query['hard_logic_py']}")
            

            self.logger.step("Starting symbolic search")
            succ, plan = self.symbolic_search(query)
        
            if succ:
                self.logger.info("Symbolic search completed successfully")
                if 'dayInfos' in plan['itinerary']:
                    for day in plan['itinerary']['dayInfos']:
                        for period in day['scheduleDetail']:
                            period['description'] = {}
                
                self.logger.debug(f"Generated plan: {plan}")
                plan_data = {
                    "itinerary_name": f"{departure} to {arrive} - {days} Day Trip",
                    "duration": days,
                    "departure_city": departure,
                    "destination_city": arrive,
                    "user_query": user_query,
                    "generated_by": "NeSy_based_system",
                    "plan":json.dumps(plan,ensure_ascii=False),
                    # "query": query,
                    "llm_response": json.dumps(plan['itinerary'],ensure_ascii=False)
                }

                execution_time = time.time() - start_time
                self.logger.performance(f"NeSy Agent execution completed in {execution_time:.2f} seconds")
                return True, plan_data
            else:
                self.logger.warning("Symbolic search failed")
                execution_time = time.time() - start_time
                self.logger.performance(f"NeSy Agent execution failed after {execution_time:.2f} seconds")
                return False, plan
                
        except Exception as e:
            self.logger.error(f"NeSy Agent execution failed with error: {str(e)}")
            execution_time = time.time() - start_time
            self.logger.performance(f"NeSy Agent execution failed after {execution_time:.2f} seconds")
            return False, {"error": str(e)}
            

    def symbolic_search(self, symoblic_query):
        self.logger.step("Starting symbolic search process")
        
        if self.preference_search:
            self.logger.info("Preference search enabled")
            preference_py = symoblic_query["preference_py"][0]
            index = preference_py.find("\n")

            concept = preference_py[:index]
            code = preference_py[index + 1:]

            symoblic_query["preference_opt"] = concept.split(" ")[0]
            symoblic_query["preference_concept"] = concept.split(" ")[1]
            symoblic_query["preference_code"] = code
            
            self.logger.debug(f"Preference optimization: {symoblic_query['preference_opt']}")
            self.logger.debug(f"Preference concept: {symoblic_query['preference_concept']}")
            self.logger.debug(f"Preference code: {symoblic_query['preference_code']}")

            if symoblic_query["preference_opt"] == "maximize":
                self.least_plan_logic_pvalue = -19260817  
            elif symoblic_query["preference_opt"] == "minimize":
                self.least_plan_logic_pvalue = 19260817  
            else:
                raise ValueError("preference_opt must be maximize or minimize")

        # get corresponding POI information
        self.poi_analyzer.load_pool_from_dict(symoblic_query)
        self.memory["accommodations"] = self.poi_analyzer.hotel_pool
        self.memory["attractions"] = self.poi_analyzer.poi_pool
        self.memory["transports"] = self.poi_analyzer.transportation_pool

        self.query = symoblic_query

        # generate plan
        success, plan = self.generate_plan_with_search(symoblic_query)

        return success, plan

    def generate_plan_with_search(self, query:Dict):

        source_city = query['start_city']
        target_city = query['target_city']
        print(target_city,type(target_city))
        if isinstance(target_city,str):
            target_city_list = target_city.split(",")
        else:
            target_city_list = target_city
        query['target_city_list'] = target_city_list

        self.time_before_search = time.time()
        self.llm_inference_time_count = 0

        # reset the cache before searching
        poi_plan = {}
        self.restaurants_visiting = []
        self.attractions_visiting = []
        self.food_type_visiting = []
        self.spot_type_visiting = []
        self.attraction_names_visiting = []
        self.restaurant_names_visiting = []
        self.ranking_attractions_flag = False
        self.ranking_restaurants_flag = False

        self.llm_rec_format_error = 0
        self.llm_rec_count = 0
        self.search_nodes = 0
        self.backtrack_count = 0

        self.constraints_validation_count = 0
        self.commonsense_pass_count = 0
        self.logical_pass_count = 0
        self.all_constraints_pass = 0

        self.least_plan_schema, self.least_plan_comm, self.least_plan_logic = None, None, None
        self.least_plan_logical_pass = -1

        # multi-destination route optimization
        if len(target_city_list) > 1:
            self.TIME_CUT = 60 * 5* len(target_city_list)
            return self.generate_multi_destination_plan(query, source_city, target_city_list)
        else:
            # single-destination processing logic remains unchanged
            return self.generate_single_destination_plan(query, source_city, target_city_list[0])

    def generate_multi_destination_plan(self, query: Dict, source_city: str, target_city_list: List[str]):
        """
        generate multi-destination trip plan
        """
        print(f"Planning multi-destination trip: {source_city} -> {' -> '.join(target_city_list)}")
        
        # optimize city visit order
        optimized_route = self.optimize_city_route(source_city, target_city_list, query)
        print(f"Optimized route: {' -> '.join(optimized_route)}")
        
        # calculate the number of days each city stays
        days_per_city = self.allocate_days_per_city(query['day'], len(optimized_route))
        print(f"Days allocation: {dict(zip(optimized_route, days_per_city))}")
        
        # generate sub-plan for each city segment
        all_city_plans = []
        current_start_city = source_city
        total_plan = {"itinerary": {"dayInfos": []}}
        total_plan['people_number'] = 0
        total_plan['start_city'] = source_city
        total_plan['target_city'] = target_city_list    
        total_plan['search_time_sec'] = 0
        total_plan['llm_inference_time_sec'] = 0
        
        current_day_offset = 0
        
        for i, city in enumerate(optimized_route):
            city_days = days_per_city[i]
            if city_days == 0:
                continue
                
            print(f"\nPlanning for city: {city} ({city_days} days)")
            
            # create single-city query
            city_query = query.copy()
            city_query['start_city'] = current_start_city
            city_query['target_city'] = city
            city_query['day'] = city_days
            city_query['departure'] = current_start_city
            city_query['arrive'] = city
            city_query['hard_logic_py'] = []
            # update dsl statement
            for dsl in query['hard_logic_py']:
                if 'day_count' in dsl and len(dsl) < 30:
                    city_query['hard_logic_py'].append(f"result=(day_count(plan)=={city_days})")
                elif 'cities_to_visit' in dsl:
                    continue
                else:
                    city_query['hard_logic_py'].append(dsl)

            # generate single-city plan
            success, city_plan_origin = self.generate_single_destination_plan(city_query, current_start_city, city)
            
            if success:
                # adjust the number of days offset
                # delete the return transportation of the last day
                city_plan = city_plan_origin['itinerary']
                city_plan = self.trace_back(city_plan, city_days-1)
                adjusted_plan = self.adjust_plan_days(city_plan, current_day_offset)
                all_city_plans.append(adjusted_plan)
                
                # merge into the total plan
                total_plan['itinerary']["dayInfos"].extend(adjusted_plan["dayInfos"])
                total_plan['search_time_sec'] += city_plan_origin['search_time_sec']
                total_plan['llm_inference_time_sec'] += city_plan_origin['llm_inference_time_sec']
                current_day_offset += city_days
                current_start_city = city
            else:
                print(f"Failed to generate plan for city: {city}")
                return False, {"error_info": f"Failed to plan city: {city}"}
        
        # add return transportation
        if current_start_city != source_city:
            return_plan = self.add_return_transportation(total_plan['itinerary'], current_start_city, source_city, query['day']-1, query)
        
        # validate the overall plan
        res_bool, res_plan = self.constraints_validation(query, return_plan, {})
        if res_bool:
            return True, res_plan
        else:
            total_plan = self.trace_back(total_plan['itinerary'], query['day']-1)  # if the innercity transportation does not satisfy the constraints, backtrack
            self.backtrack_count += 1
            print(
                "Back-transport, but constraints_validation failed, backtrack..."
            )
            return False, total_plan
        

    def optimize_city_route(self, source_city: str, target_cities: List[str], query: Dict) -> List[str]:
        """
        optimize the city visit route, considering the location and transportation convenience
        """
        
        if len(target_cities) <= 1:
            return target_cities

        
        optimized_route = []
        remaining_cities = target_cities.copy()
        current_city = source_city
        if source_city in remaining_cities:
            remaining_cities.remove(source_city)
            optimized_route.append(source_city)
        
        max_iter = 100
        while remaining_cities:
            # find the next city closest to the current city
            best_city = None
            best_score = 0.7
            
            for city in remaining_cities:
                # check if there is a direct transportation connection
                transport_info = self.collect_intercity_transport(current_city, city, "train")
                
                # calculate the comprehensive score (transport convenience + location)
                transport_score = 1.0 if transport_info else 0.5
                
                if transport_score > best_score:
                    best_score = transport_score
                    best_city = city
                if best_city:
                    optimized_route.append(best_city)
                    remaining_cities.remove(best_city)
                    current_city = best_city
            max_iter -= 1
            if max_iter <= 0:
                break
        if not optimized_route or max_iter <0:
            return remaining_cities
        
        return optimized_route

    def allocate_days_per_city(self, total_days: int, num_cities: int) -> List[int]:
        """
        allocate the number of days each city stays
        """
        if num_cities == 0:
            return []
        
        # basic allocation: each city stays at least 1 day
        base_days = max(1, total_days // num_cities)
        remaining_days = total_days - (base_days * num_cities)
        
        days_allocation = [base_days] * num_cities
        
        # allocate the remaining days to the first few cities
        for i in range(remaining_days):
            days_allocation[i] += 1
        
        return days_allocation

    def adjust_plan_days(self, plan: Dict, day_offset: int) -> Dict:
        """
        adjust the number of days offset in the plan
        """
        adjusted_plan = deepcopy(plan)
        
        for day_info in adjusted_plan["dayInfos"]:
            day_info["day"] += day_offset
        
        return adjusted_plan

    def add_return_transportation(self, plan: Dict, current_city: str, source_city: str, day_offset: int, query: Dict) -> Dict:
        """
        add return transportation
        """
        return_transport = self.collect_intercity_transport(current_city, source_city, "train")

        if not return_transport:
            return_transport = {
                "id": str(self.unique_id),
                "name": "transportation-{}".format(str(self.unique_id)),
                "type": "resource_pool",
                "segments": [
                    {
                        "tripType": "driving",
                        "fromStationCode": "",
                        "toStationCode": "",
                        "departureTime": "18:00",
                        "arrivalTime": "20:00",
                    }
                ],
                "start_time": "18:00",
                "end_time": "20:00",
                "start_districtid": None,
                "end_districtid": None,
                "minutes": 120,
                "price": 0,
                "trip_type": "OUTBOUND",
                "key": f"{current_city}->{source_city}",
                "t_type": "back",
                "depature_time": "18:00",
                "flight_time": "20:00",
                "planid": str(self.unique_id),
                "cardType": "driving"
            }
            self.unique_id += 1
            query = self.update_transport_pool(query, return_transport)
        else:
            return_transport = return_transport[0]

        # add return transportation to the last day
        plan["dayInfos"][day_offset]["scheduleDetail"][-1]["detailList"] = self.add_intercity_transport(
            plan["dayInfos"][day_offset]["scheduleDetail"][-1]["detailList"],
            return_transport,
            innercity_transports=[],
            tickets=self.query["people_number"],
        )

        return plan

    def generate_single_destination_plan(self, query: Dict, source_city: str, target_city: str):
        """
        generate single-destination plan (the original logic is encapsulated)
        """
        poi_plan = {}
        query['target_city'] = target_city
        print(source_city, "->", target_city)
        
        original_query = self.query
        self.query = query

        #==================== intercity transportation: train and plane
        train_go = self.collect_intercity_transport(source_city, target_city, "train")  
        train_back = self.collect_intercity_transport(target_city, source_city, "train")

        go_info = train_go 
        back_info = train_back

        print(
            "from {} to {}: {} trans".format(
                source_city, target_city, len(go_info)
            )
        )
        print(
            "from {} to {}: {} trans".format(
                target_city, source_city, len(back_info)
            )
        )
        # if there is no transportation information, default to driving
        if len(go_info) == 0:
            init_go_info = {
                "id": str(self.unique_id),
                "name": "transportation-{}".format(str(self.unique_id)),
                "type": "resource_pool",
                "segments": [
                    {
                    "tripType": "driving",
                    "fromStationCode": "",
                    "toStationCode": "",
                    "departureTime": "09:00",
                    "arrivalTime": "11:00",
                    "flightNo": "",
                    "fromStationName": source_city,
                    "toStationName": target_city
                    }
                ],
                "start_time": "09:00",
                "end_time": "11:00",
                "start_districtid": None,
                "end_districtid": None,
                "minutes": 120,
                "price": 0,
                "trip_type": "OUTBOUND",
                "key": f"{source_city}->{target_city}",
                "t_type": "to",
                "depature_time": "09:00",
                "flight_time": "11:00",
                "planid": str(self.unique_id),
                "cardType": "driving"
                }
            self.unique_id += 1
            go_info.append(init_go_info)
            query = self.update_transport_pool(query, init_go_info)
        if len(back_info) == 0:
            init_back_info = {
                "id": str(self.unique_id),
                "name": "transportation-{}".format(str(self.unique_id)),
                "type": "resource_pool",
                "segments": [
                    {
                    "tripType": "driving",
                    "fromStationCode": "",
                    "toStationCode": "",
                    "departureTime": "18:00",
                    "arrivalTime": "20:00",
                    "flightNo": "",
                    "fromStationName": target_city,
                    "toStationName": source_city
                    }
                ],
                "start_time": "18:00",
                "end_time": "20:00",
                "start_districtid": None,
                "end_districtid": None,
                "minutes": 120,
                "price": 0,
                "trip_type": "OUTBOUND",
                "key": f"{target_city}->{source_city}",
                "t_type": "back",
                "depature_time": "18:00",
                "flight_time": "20:00",
                "planid": str(self.unique_id),
                "cardType": "driving"
                }
            self.unique_id += 1
            back_info.append(init_back_info)
            query = self.update_transport_pool(query, init_back_info)
        
        ranking_go = self.ranking_intercity_transport_go(go_info, query)  # according to the departure time and price
        ranking_go = self.reranking_intercity_transport_go_with_constraints(
            ranking_go, go_info, query
        )

        ranking_hotel = self.ranking_hotel(self.memory["accommodations"], query)
        query_room_number, query_room_type = self.decide_rooms(query)
        self.required_budget = self.extract_budget(query)

        ranking_hotel = self.reranking_hotel_with_constraints(
            ranking_hotel, self.memory["accommodations"], query, query_room_number
        )

        self.innercity_transports_ranking_from_query = self.ranking_innercity_transport_from_query(query)

        # ====================generate plan based on the intercity-transport and hotel
        for go_i in ranking_go:
            go_info_i = go_info[go_i]
            poi_plan["go_transport"] = go_info_i  # record the go transportation information
            self.search_nodes += 1

            ranking_back = self.ranking_intercity_transport_back(
                back_info, query, go_info_i
            )

            ranking_back = self.reranking_intercity_transport_back_with_constraints(
                ranking_back, back_info, query, go_info_i
            )

            for back_i in ranking_back:
                back_info_i = back_info[back_i]
                poi_plan["back_transport"] = back_info_i
                self.search_nodes += 1

                if query["day"] > 1:
                    for hotel_i in ranking_hotel:
                        poi_plan["accommodation"] =self.poi_analyzer.get_hotel_info(self.memory["accommodations"][hotel_i], query["locale"])  # id
                        room_type = 2 
                        self.search_nodes += 1

                        required_rooms = (int((query["people_number"] - 1) / room_type) + 1)

                        if query_room_type != None and query_room_type != room_type:
                            self.backtrack_count += 1
                            print("room_type not match, backtrack...")
                            continue

                        if query_room_number != None:
                            required_rooms = query_room_number

                        if query_room_number != None and query_room_type != None:
                            pass
                        else:
                            # if there is no specified room type, expect the minimum number of rooms
                            if (
                                    room_type * required_rooms >= query["people_number"]
                            ) and (
                                    room_type * required_rooms < query["people_number"] + room_type
                            ):
                                pass
                            else:
                                if query_room_number != None and room_type == 2:
                                    pass
                                else:
                                    self.backtrack_count += 1
                                    print("room_number * room_type not match, backtrack...")
                                continue
                        self.required_rooms = required_rooms
                  
                        transport_cost = (poi_plan["go_transport"]["price"] + poi_plan["back_transport"]["price"]) * query["people_number"]
                        hotel_cost = float(poi_plan["accommodation"]["price"])*required_rooms*(query["day"] - 1)
                        self.intercity_with_hotel_cost = transport_cost + hotel_cost
                        if (
                                self.required_budget != None
                                and self.required_budget - self.intercity_with_hotel_cost
                                <= self.query["people_number"]
                                * (self.query["day"] - 1)
                                * 100
                        ):
                            self.backtrack_count += 1
                            print(
                                "required_budget - intercity_with_hotel_cost <= 100 * people_number * (days-1), backtrack...")
                            continue

                        print("search: ...")
                        try:
                            # depth first search algorithm
                            success, plan = self.dfs_poi(  
                                query,
                                poi_plan,  # candidate poi
                                plan=[],  # current plan
                                current_time="",  # current time
                                current_position="",  # current position
                            )
                        except TimeOutError as e:
                            print("TimeOutError")
                            return False, {"error_info": "TimeOutError"}
                        # exit(0)

                        print(success, plan)
                        if success:
                            # restore the original self.query
                            self.query = original_query
                            return True, plan
                        else:
                            if time.time() > self.time_before_search + self.TIME_CUT:
                                print("Searching TIME OUT !!!")
                                return False, {"error_info": "TimeOutError"}

                            self.backtrack_count += 1
                            print("search failed given the intercity-transport and hotels, backtrack...")

                else:
                    if TimeUtils.compare_times_later(
                            poi_plan["back_transport"]["depature_time"],
                            poi_plan["go_transport"]["flight_time"])==-1:
                        self.backtrack_count += 1
                        print("back_transport depature_time earlier than go_transport arrival_time, backtrack...")
                        continue

                    self.intercity_with_hotel_cost = (
                                                            poi_plan["go_transport"]["price"]
                                                            + poi_plan["back_transport"]["price"]
                                                    ) * query["people_number"]
                    print("search: ...")
                    try:
                        success, plan = self.dfs_poi(
                            query,
                            poi_plan,
                            plan=[],
                            current_time="",
                            current_position="",
                        )
                    except TimeOutError as e:
                        print("TimeOutError")
                        return False, {"error_info": "TimeOutError"}

                    print(success, plan)
                    if success:
                        # restore the original self.query
                        self.query = original_query
                        return True, plan
                    else:
                        if time.time() > self.time_before_search + self.TIME_CUT:
                            print("Searching TIME OUT !!!")
                            return False, {"error_info": "TimeOutError"}

                        self.backtrack_count += 1
                        print("search failed given the intercity-transport and hotels, backtrack...")


        # restore the original self.query
        self.query = original_query
        
        source_city = target_city 
        return False, {"error_info": "No solution found."}





    def constraints_validation(self, query, plan, poi_plan):
        self.logger.constraint(f"Starting constraints validation (attempt #{self.constraints_validation_count + 1})")
        self.constraints_validation_count += 1

        res_plan = {
            "people_number": query["people_number"],
            "start_city": query["start_city"],
            "target_city": query["target_city"],
            "itinerary": plan,
        }
        self.logger.debug(f"Plan to validate: {plan}")

        self.least_plan_schema = deepcopy(res_plan)

        bool_result = func_commonsense_constraints(query, res_plan['itinerary'], verbose=True)  # commonsense constraints


        if bool_result:
            self.commonsense_pass_count += 1


        logical_result = evaluate_constraints_py(query, res_plan['itinerary'], verbose=True)  # logical constraints

        print("logical_result",logical_result)

        logical_pass = True
        for idx, item in enumerate(logical_result):
            logical_pass = logical_pass and item  # whether the logical constraints pass

            if item:
                print(query["hard_logic_py"][idx], "passed!")
            else:

                print(query["hard_logic_py"][idx], "failed...")
        if bool_result and np.sum(logical_result) > self.least_plan_logical_pass:   # if the commonsense constraints pass, and the number of logical constraints that pass is greater than the current optimal, then update the optimal
            self.least_plan_comm = deepcopy(res_plan) 
            self.least_plan_logical_pass = np.sum(logical_result)  

        if logical_pass:
            self.logical_pass_count += 1

        bool_result = bool_result and logical_pass  # whether the commonsense constraints and logical constraints pass
        # bool_result = True
        if bool_result:
            print("\n Pass! \n")
            self.all_constraints_pass += 1

            if self.least_plan_logic is None:
                self.least_plan_logic = res_plan

        else:
            print("\n Failed \n")

        if self.preference_search:
            return False, plan

        if bool_result:
            res_plan["search_time_sec"] = time.time() - self.time_before_search
            res_plan["llm_inference_time_sec"] = self.llm_inference_time_count
            return True, res_plan
        else:
            return False, plan

    def add_intercity_transport(
            self, activities:List[Dict], intercity_info:Dict, innercity_transports:List[Dict]=[], tickets:int=1 )->List[Dict]:
        activity_i = {
            'id': intercity_info["planid"],
            'type': 'transportation',
            'name': intercity_info["planid"], 
            "start_time": intercity_info["depature_time"],
            "end_time": intercity_info["flight_time"],
            "start": intercity_info["key"].split("->")[0],
            "end": intercity_info["key"].split("->")[1],
            "price": intercity_info["price"],
            "cost": intercity_info["price"] * tickets,
            "tickets": tickets,
            "transports": innercity_transports,
            
        }

        activities.append(activity_i)
        return activities


    def add_poi(
            self,
            activities:List[Dict],
            id:str,
            name:str,
            type:str,
            price:int,
            cost:int,
            start_time:str,
            end_time:str,
            innercity_transports:List[Dict],):
        activity_i = {
            'id': id,
            'name': name,
            'type': type,
            "price": price,
            "cost": cost,
            "start_time": start_time,
            "end_time": end_time,
            "transports": innercity_transports,
        }

        activities.append(activity_i)
        return activities

    def add_accommodation(
            self,
            current_plan:List[Dict],
            hotel_sel:Dict,
            current_day:int,
            arrived_time:str,
            required_rooms:int,
            transports_sel:List[Dict],
     )->List[Dict]:

        current_plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"] = self.add_poi(
            activities=current_plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"],
            id=hotel_sel['id'],
            name=hotel_sel["cname"] if self.query["locale"] == 'zh-CN' else hotel_sel["ename"],
            type="hotel",
            price=hotel_sel["price"],
            cost=int(hotel_sel["price"]) * required_rooms,
            start_time=arrived_time,
            end_time="24:00",
            innercity_transports=transports_sel,
        )
        current_plan["dayInfos"][current_day]["scheduleDetail"][0]["detailList"][-1]["room_type"] = 2 
        current_plan["dayInfos"][current_day]["scheduleDetail"][0]["detailList"][-1]["rooms"] = required_rooms

        return current_plan

    def add_attraction(
            self, current_plan, poi_type, poi_sel, current_day, arrived_time, transports_sel, poi_hours
        ):

        # opening time
        open_time = self.poi_analyzer.parse_poi_time_window(poi_sel)
        opentime, endtime = (open_time.split('-')[0].strip(),open_time.split('-')[1].strip()) 

        # it is closed ...
        if TimeUtils.compare_times_later(arrived_time,endtime )>0:
            raise Exception("Add POI error")

        if TimeUtils.compare_times_later(opentime, arrived_time)>0:
            act_start_time = opentime
        else:
            act_start_time = arrived_time

        poi_time = poi_hours[0]
        act_end_time = TimeUtils.time_operation_add(act_start_time, poi_time)
        if TimeUtils.compare_times_later(act_end_time,endtime)>0:
            act_end_time = endtime

        tmp_plan = deepcopy(current_plan)
        period = TimeUtils.get_time_of_day(act_start_time)[1][0]
        if period not in  [i['period'] for i in tmp_plan['dayInfos'][current_day]["scheduleDetail"]]:
            tmp_plan['dayInfos'][current_day]["scheduleDetail"].append({"period": period, "detailList": []})
        tmp_plan['dayInfos'][current_day]["scheduleDetail"][-1]["detailList"] = self.add_poi(
            activities=tmp_plan['dayInfos'][current_day]["scheduleDetail"][-1]["detailList"],
            id=poi_sel["id"].replace("poi_",""),
            name=poi_sel["name"],
            type='poi',
            price=int(poi_sel["price"]),
            cost=int(poi_sel["price"]) * self.query["people_number"],
            start_time=act_start_time,
            end_time=act_end_time,
            innercity_transports=transports_sel,
        )
        tmp_plan['dayInfos'][current_day]["scheduleDetail"][-1]["detailList"][-1]["tickets"] = self.query["people_number"]

        return tmp_plan

    def check_if_too_late(
            self, query, current_day, current_time, current_position, poi_plan
        ):

        if current_time != "" and TimeUtils.compare_times_later("23:00", current_time)==-1:
            print("too late, after 23:00")
            return True

        if current_time != "" and current_day == query["day"] - 1:
            # We should go back in time ...
            transports_ranking = self.innercity_transports_ranking_from_query  # inner-city transportation

            for transport_type_sel in transports_ranking:

                self.search_nodes += 1
                try:
                    back_position = (poi_plan["back_transport"]["from"]["lat"], poi_plan["back_transport"]["from"]["lng"])
                except:
                    back_position = current_position
                flag = True
                if "back_transport" in poi_plan:
                    transports_sel = self.collect_innercity_transport(
                        query["arrive"],
                        current_position,
                        back_position,
                        current_time,
                        transport_type_sel,
                    )
                    if not isinstance(transports_sel, list):
                        self.backtrack_count += 1
                        print("inner-city transport error, backtrack...")
                        continue

                    if len(transports_sel) > 0:
                        arrived_time = transports_sel[-1]["end_time"]
                    else:
                        arrived_time = current_time

                    if  TimeUtils.compare_times_later(poi_plan["back_transport"]["depature_time"],arrived_time)>=0 :
                        flag = False
                if flag:
                    print(
                        "Can not go back source-city in time, current POI {}, station arrived time: {}".format(
                            current_position, arrived_time
                        )
                    )
                    return True

        elif current_time != "":
            if "accommodation" in poi_plan:
                hotel_sel = poi_plan["accommodation"]
                transports_ranking = self.innercity_transports_ranking_from_query

                for transport_type_sel in transports_ranking:
                    self.search_nodes += 1
                    flag = True
                    if "back_transport" in poi_plan:
                        transports_sel = self.collect_innercity_transport(
                            query["arrive"],
                            current_position,
                            (hotel_sel["lat"], hotel_sel["lon"]),
                            current_time,
                            transport_type_sel,
                        )
                        if not isinstance(transports_sel, list):
                            self.backtrack_count += 1
                            print("inner-city transport error, backtrack...")
                            continue

                        flag = True

                        if len(transports_sel) > 0:
                            arrived_time = transports_sel[-1]["end_time"]
                        else:
                            arrived_time = current_time
                        if TimeUtils.compare_times_later(arrived_time, "24:00") >= 0:
                            flag = False
                    if flag:
                        print(
                            "Can not go back to hotel, current POI {}, hotel arrived time: {}".format(
                                current_position, arrived_time
                            )
                        )
                        return True

        return False

    def reranking_intercity_transport_go_with_constraints( self, ranking_go:List[int], go_info:List[Dict], query:Dict)->List[int]:

        ### check constraints
        pass_num_list = np.zeros(len(go_info))

        for go_i in ranking_go:
            go_sel = go_info[go_i]
            tmp_plan = {"dayInfos": [{"day": 1, "scheduleDetail": [{"period": "Morning", "detailList": []}]}]}
            # add transportation
            tmp_plan["dayInfos"][0]["scheduleDetail"][0]["detailList"] = self.add_intercity_transport(
                tmp_plan["dayInfos"][0]["scheduleDetail"][0]["detailList"],
                go_sel,
                innercity_transports=[],
                tickets=self.query["people_number"],
            )

            res_plan = {
                "people_number": query["people_number"],
                "start_city": query["departure"],
                "target_city": query["arrive"],
                "itinerary": tmp_plan,
            }
            # evaluate constraints run constraint code, get the result of whether the res_plan satisfies the constraints
            logical_result = evaluate_constraints_py(query, res_plan['itinerary'])  # 1 correct, 0 incorrect

            # print(logical_result)

            pass_num_list[go_i] = np.sum(logical_result)
        if len(pass_num_list) == 0:
            return ranking_go
        pass_maxx = int(np.max(pass_num_list))  # maximum number of passes 2
        reranking_list = []
        if pass_maxx > 0:
            for p_i in range(pass_maxx, -1, -1):
                for idx in ranking_go:
                    if pass_num_list[idx] == p_i:
                        reranking_list.append(idx)
        else:
            reranking_list = ranking_go

        return reranking_list

    def reranking_intercity_transport_back_with_constraints(
            self, ranking_back:List[int], back_info:List[Dict], query:Dict, go_sel:Dict
        ):

        ### check constraints
        pass_num_list = np.zeros(len(back_info))

        for back_i in ranking_back:

            back_sel = back_info[back_i]
            tmp_plan = {"dayInfos": [{"day": 1, "scheduleDetail": [{"period": "Afternoon", "detailList": []}]}]}
            tmp_plan["dayInfos"][0]["scheduleDetail"][0]["detailList"] = self.add_intercity_transport(
                tmp_plan["dayInfos"][0]["scheduleDetail"][0]["detailList"],
                go_sel,
                innercity_transports=[],
                tickets=self.query["people_number"],
            )
            if query["day"] > 1:
                for dayy in range(1, query["day"]):
                    tmp_plan["dayInfos"].append({"day": dayy + 1, "scheduleDetail": [{"period": "Afternoon", "detailList": []}]})
            tmp_plan["dayInfos"][-1]["scheduleDetail"][0]["detailList"] = self.add_intercity_transport(
                tmp_plan["dayInfos"][-1]["scheduleDetail"][0]["detailList"],
                back_sel,
                innercity_transports=[],
                tickets=self.query["people_number"],
            )

            res_plan = {    
                "people_number": query["people_number"],
                "start_city": query["departure"],
                "target_city": query["arrive"],
                "itinerary": tmp_plan,
            }
            logical_result = evaluate_constraints_py(query, res_plan['itinerary'])

            pass_num_list[back_i] = np.sum(logical_result)

        pass_maxx = int(np.max(pass_num_list))

        reranking_list = []
        if pass_maxx > 0:
            for p_i in range(pass_maxx, -1, -1):
                for idx in ranking_back:
                    if pass_num_list[idx] == p_i:
                        reranking_list.append(idx)
        else:
            reranking_list = ranking_back

        return reranking_list

    def reranking_hotel_with_constraints(
            self, ranking_hotel:List[int], hotel_info:List[Dict], query:Dict, query_room_number:int
        ):

        pass_num_list = np.zeros(len(hotel_info))
        ### check constraints

        for idx in range(len(hotel_info)):
            hotel_id = hotel_info[idx]
            hotel_sel = self.poi_analyzer.get_hotel_info(hotel_id, query["locale"])

            if query_room_number is None:
                room_type = 2 
                required_rooms = int((query.get("people_number", 1) - 1) / room_type) + 1
            else:
                required_rooms = query_room_number

            plan = {"dayInfos": []}
            # add hotel every day
            for dayy in range(query["day"] - 1):
                plan["dayInfos"].append({"day": dayy + 1, "scheduleDetail": [{"period": "Evening", "detailList": []}]})
                plan = self.add_accommodation(
                    current_plan=plan,
                    hotel_sel=hotel_sel,
                    current_day=dayy,
                    arrived_time="20:00",
                    required_rooms=required_rooms,
                    transports_sel=[],
                )

            res_plan = {
                "people_number": query["people_number"],
                "start_city": query["departure"],
                "target_city": query["arrive"],
                "itinerary": plan,
            }
            logical_result = evaluate_constraints_py(query, res_plan['itinerary'])
            pass_num_list[idx] = np.sum(logical_result)

        pass_maxx = int(np.max(pass_num_list))

        reranking_list = []
        if pass_maxx > 0:
            for p_i in range(pass_maxx, -1, -1):
                for idx in ranking_hotel:
                    if pass_num_list[idx] == p_i:
                        reranking_list.append(idx)
        else:
            reranking_list = ranking_hotel

        return reranking_list

    def reranking_attractions_with_constraints(
            self,
            plan,
            poi_type,
            current_day,
            current_time,
            current_position,
            attr_info,
            query,
            ranking_attractions,
        ):

        pass_num_list = []
        ### check constraints

        for idx in range(len(attr_info)):
            poi_sel, min_spend_time, poi_info = self.poi_analyzer.read_poi_api_info(attr_info[idx], query["locale"])
            self.search_nodes += 1
            new_position = (poi_sel['lat'], poi_sel['lon'])
            poi_info['id'] = poi_sel['id']
            poi_info['name'] = poi_sel['cname']
            poi_info['price'] = poi_sel['price']
            if (new_position) == current_position or current_position == (0,0):
                transports_sel = []
                arrived_time = current_time
            else:
                transports_sel = self.collect_innercity_transport(
                    query["target_city"],
                    current_position,
                    new_position,
                    current_time,
                    "taxi",
                )
                if not isinstance(transports_sel, list):
                    self.backtrack_count += 1
                    print("inner-city transport error, backtrack...")
                    continue

                if len(transports_sel) == 0:
                    arrived_time = current_time
                else:
                    arrived_time = transports_sel[-1]["end_time"]

            try:
                tmp_plan = self.add_attraction(
                    plan, poi_type, poi_info, current_day, arrived_time, transports_sel,self.poi_analyzer.calculate_poi_hours(poi_sel)
                )
                res_plan = {
                    "people_number": query["people_number"],
                    "start_city": query["start_city"],
                    "target_city": query["target_city"],
                    "itinerary": tmp_plan,
                }
                logical_result = evaluate_constraints_py(
                    query, res_plan['itinerary'] 
                )
                pass_num_list.append(np.sum(logical_result))
            except:
                pass_num_list.append(0)
        pass_maxx = np.max(pass_num_list)

        reranking_list = []
        if pass_maxx > 0:
            for p_i in range(pass_maxx, -1, -1):
                for idx in ranking_attractions: 
                    if pass_num_list[idx] == p_i:
                        reranking_list.append(idx)
        else:
            reranking_list = ranking_attractions

        return reranking_list

    def dfs_poi(
        self, query, poi_plan, plan, current_time, current_position, current_day=0
        ):
        self.logger.search(f"DFS POI search - Day {current_day}, Time {current_time}, Position {current_position}")
        self.search_nodes += 1

        if (
            time.time() - self.time_before_search
            > self.TIME_CUT # + self.llm_inference_time_count
        ):
            self.logger.warning(f"Search timeout after {self.TIME_CUT} seconds")
            raise TimeOutError

        if self.check_if_too_late(
            query, current_day, current_time, current_position, poi_plan
        ):
            self.backtrack_count += 1
            self.logger.search("Current time is too late for hotel or back-transport, backtracking")
            return False, plan

        if self.required_budget != None:
            total_cost = 0
            for day_activities in plan:
                for activity in day_activities["activities"]:
                    if activity["type"] in [
                        "breakfast",
                        "lunch",
                        "dinner",
                        "attraction",
                    ]:
                        total_cost += activity["cost"]

            if total_cost + self.intercity_with_hotel_cost > self.required_budget:
                self.backtrack_count += 1
                print("budget exceeded, backtrack...")
                return False, plan

        # intercity_transport - go # check the time/budget of the departure transportation
        if current_day == 0 and current_time == "":
            go_transport_starttime = poi_plan["go_transport"]["depature_time"]
            go_transport_endtime = poi_plan["go_transport"]["flight_time"]
            # add departure transportation
            period = TimeUtils.get_time_of_day(go_transport_starttime)[1][0]
            plan = {"dayInfos": [{"day": current_day + 1, "scheduleDetail": [{"period": period, "detailList": []}]}]}
            plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"] = self.add_intercity_transport(
                plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"],
                poi_plan["go_transport"],
                innercity_transports=[],
                tickets=self.query["people_number"],
            )
            
            if TimeUtils.parse_time(go_transport_starttime).day != TimeUtils.parse_time(go_transport_endtime).day:  # if cross day  # add a new day plan
                new_period = TimeUtils.get_time_of_day(go_transport_endtime)[1][0]    
                plan["dayInfos"].append({"day": current_day + 2, "scheduleDetail": [{"period": new_period, "detailList": []}]})
                current_day += 1

            new_time = go_transport_endtime
            new_position = (0,0)
            try:
                new_position = (poi_plan["go_transport"]["to"]["lat"], poi_plan["go_transport"]["to"]["lng"])
            except:
                new_position = (0,0)
            success, plan = self.dfs_poi(
                query, poi_plan, plan, new_time, new_position, current_day
            )
            if success:
                return True, plan
            else:
                self.backtrack_count += 1
                print("No solution for the given Go Transport, backtrack...")
                return False, plan

        # breakfast
        if current_time == "00:00":
            period = 'Morning'
            if len(plan["dayInfos"]) < current_day + 1:
                plan["dayInfos"].append({"day": current_day + 1, "scheduleDetail": [{"period": period, "detailList": []}]})

            self.search_nodes += 1
            new_time = TimeUtils.time_operation_add(current_time, 8)
            new_position = current_position
            success, plan = self.dfs_poi(
                query, poi_plan, plan, new_time, new_position, current_day
            )
            if success:
                return True, plan
            plan = self.trace_back(plan, current_day)

            candidates_type = []
            if current_day == query["day"] - 1 and current_time != "":
                candidates_type.append("back-intercity-transport")
            else:

                self.backtrack_count += 1
                print("No solution for the given Breakfast, backtrack...")

                return False, plan

        else:
            # period = TimeUtils.get_time_of_day(current_time)[1][0]
            candidates_type = ["attraction"]
            if ("accommodation" in poi_plan) and (current_day < query["day"] - 1):
                candidates_type.append("hotel")
            if current_day == query["day"] - 1 and current_time != "":
                candidates_type.append("back-intercity-transport")

        print("candidates_type: ", candidates_type)

        while len(candidates_type) > 0:
            # select the next poi
            poi_type, candidates_type = self.select_next_poi_type(
                candidates_type,
                plan,
                poi_plan,
                current_day,
                current_time,
                current_position,
            )

            print(
                "POI planning, day {} {}, {}, next-poi type: {}".format(
                    current_day+1, current_time, current_position, poi_type
                )
            )
            # update plan
            if poi_type == "back-intercity-transport":
                period = TimeUtils.get_time_of_day(poi_plan["back_transport"]['depature_time'])[1][0]
                plan = self.delete_empty_period(plan, current_day)
                if len(plan["dayInfos"]) < current_day + 1:
                    plan["dayInfos"].append({"day": current_day + 1, "scheduleDetail": [{"period": period, "detailList": []}]})
                if period not in  [i['period'] for i in plan["dayInfos"][current_day]["scheduleDetail"]]:
                    plan["dayInfos"][current_day]["scheduleDetail"].append({"period": period, "detailList": []})

                back_transport = poi_plan["back_transport"]
                try:
                    new_position = (back_transport["to"]["lat"], back_transport["to"]["lng"])
                except:
                    new_position = current_position
                transports_ranking = self.innercity_transports_ranking_from_query
                for trans_type_sel in transports_ranking:
                    self.search_nodes += 1
                    transports_sel = self.collect_innercity_transport(  # inner-city transportation time and cost
                        query["target_city"],
                        current_position,
                        new_position,
                        current_time,
                        trans_type_sel,
                    )
                    if not isinstance(transports_sel, list):
                        self.backtrack_count += 1
                        print("inner-city transport error, backtrack...")
                        continue

                    plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"] = self.add_intercity_transport(
                        plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"],
                        poi_plan["back_transport"],
                        innercity_transports=transports_sel,
                        tickets=self.query["people_number"],
                    )  # add inner-city transportation

                    
                    res_bool, res_plan = self.constraints_validation(
                        query, plan, poi_plan
                    )

                    if res_bool:
                        return True, res_plan
                    else:
                        plan = self.trace_back(plan, current_day)  # if the inner-city transportation does not satisfy the constraints, then backtrack
                        self.backtrack_count += 1

                        print(
                            "Back-transport, but constraints_validation failed, backtrack..."
                        )
                        return False, plan
            elif poi_type == "hotel":
                period = TimeUtils.get_time_of_day(current_time)[1][0]
                plan = self.delete_empty_period(plan, current_day)
                if period not in  [i['period'] for i in plan["dayInfos"][current_day]["scheduleDetail"]]:
                    plan["dayInfos"][current_day]["scheduleDetail"].append({"period": period, "detailList": []})
                hotel_sel = poi_plan["accommodation"]            
                transports_ranking = self.innercity_transports_ranking_from_query
                new_position = (hotel_sel['lat'], hotel_sel['lon'])
                if current_position == (0,0):
                    current_position = new_position
                for trans_type_sel in transports_ranking:
                    self.search_nodes += 1
                    if new_position == current_position:
                        transports_sel = []
                        arrived_time = current_time
                    else:
                        transports_sel = self.collect_innercity_transport(  # inner-city transportation time and cost
                            query["target_city"],
                            current_position,
                            new_position,
                            current_time,
                            trans_type_sel,
                        )
                        if not isinstance(transports_sel, list):
                            self.backtrack_count += 1
                            print("inner-city transport error, backtrack...")
                            continue

                        if len(transports_sel) == 0:
                            arrived_time = current_time
                        else:
                            arrived_time = transports_sel[-1]["end_time"]

                    plan = self.add_accommodation(
                        current_plan=plan,
                        hotel_sel=hotel_sel,
                        current_day=current_day,
                        arrived_time=arrived_time,
                        required_rooms=self.required_rooms,
                        transports_sel=transports_sel,
                    )

                    new_time = "00:00"
                    new_position = new_position

                    success, plan = self.dfs_poi(
                        query, poi_plan, plan, new_time, new_position, current_day + 1
                    )

                    if success:
                        return True, plan

                    self.backtrack_count += 1
                    print("Fail with the given accommodation activity, backtrack...")

                    plan = self.trace_back(plan, current_day)  # if the hotel does not satisfy the constraints, then backtrack
                    
            elif poi_type in ["attraction"]:
                ranking_idx = self.ranking_attractions(  # according to distance and price
                    current_time,
                    current_position,
                    self.intercity_with_hotel_cost
                )

                ranking_idx = self.reranking_attractions_with_constraints(  # return the attractions that pass the constraints most
                    plan,
                    poi_type,
                    current_day,
                    current_time,
                    current_position,
                    self.memory["attractions"],
                    query,
                    ranking_idx,
                )

                for sea_i, r_i in enumerate(ranking_idx):

                    if self.search_width != None and sea_i >= self.search_width:
                        print(
                            "Out of search_width [{}], break".format(
                                self.search_width
                            )
                        )
                        break
                    self.search_nodes += 1
                    attr_idx = r_i
                    if not (attr_idx in self.attractions_visiting):  # not repeat

                        if attr_idx < 0 or attr_idx >= len(
                                self.memory["attractions"]
                        ):
                            print(attr_idx, len(self.memory["attractions"]))

                        poi_sel, min_spend_time, poi_info = self.poi_analyzer.read_poi_api_info(self.memory["attractions"][attr_idx], query["locale"])

                        transports_ranking = (self.innercity_transports_ranking_from_query)
                        if current_position == (0,0):
                            current_position = (poi_sel['lat'], poi_sel['lon'])
                        for trans_type_sel in transports_ranking:
                            self.search_nodes += 1
                            transports_sel = self.collect_innercity_transport(
                                query["target_city"],
                                current_position,
                                (poi_sel['lat'], poi_sel['lon']),
                                current_time,
                                trans_type_sel,
                            )
                            if not isinstance(transports_sel, list):
                                self.backtrack_count += 1
                                print("inner-city transport error, backtrack...")
                                continue
                            if len(transports_sel) == 0:
                                arrived_time = current_time
                            else:
                                arrived_time = transports_sel[-1]["end_time"]

                            open_time = self.poi_analyzer.parse_poi_time_window(poi_info)
                            opentime, endtime = (open_time.split('-')[0].strip(),open_time.split('-')[1].strip()) 
                            # too late
                            if TimeUtils.compare_times_later(arrived_time,"21:00")>0:
                                self.backtrack_count += 1
                                print("The current time is too late...")
                                continue

                            # it is closed
                            if TimeUtils.compare_times_later(arrived_time,endtime)>0:
                                self.backtrack_count += 1
                                print("The attraction is closed now...")
                                continue

                            if TimeUtils.compare_times_later( opentime, arrived_time)>0:
                                act_start_time = opentime
                            else:
                                act_start_time = arrived_time

                            poi_time = self.select_poi_time(
                                plan,
                                poi_plan,
                                current_day,
                                act_start_time,
                                poi_sel["cname"],
                                poi_type,
                                self.poi_analyzer.calculate_poi_hours(poi_sel)
                            )
                            act_end_time = TimeUtils.time_operation_add(act_start_time, poi_time)
                            if TimeUtils.compare_times_later(endtime, act_end_time)==-1:
                                act_end_time = endtime

                            period = TimeUtils.get_time_of_day(act_start_time)[1][0]
                            plan = self.delete_empty_period(plan, current_day)
                            if period not in  [i['period'] for i in plan["dayInfos"][current_day]["scheduleDetail"]]:
                                plan["dayInfos"][current_day]["scheduleDetail"].append({"period": period, "detailList": []})

                            plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"] = self.add_poi(
                                activities=plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"],
                                id = poi_sel['id'].replace("poi_",""),
                                name=poi_sel['cname'],
                                type='poi',
                                price=int(poi_sel["price"]),
                                cost=int(poi_sel["price"])
                                        * self.query["people_number"],
                                start_time=act_start_time,
                                end_time=act_end_time,
                                innercity_transports=transports_sel,
                            )
                            plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"][-1]["tickets"] = (
                                self.query["people_number"]
                            )

                            new_time = act_end_time
                            new_position = (poi_sel['lat'], poi_sel['lon'])

                            self.attractions_visiting.append(attr_idx)
                            self.spot_type_visiting.append('poi')
                            self.attraction_names_visiting.append(poi_sel["cname"])

                            success, plan = self.dfs_poi(
                                query,
                                poi_plan,
                                plan,
                                new_time,
                                new_position,
                                current_day,
                            )

                            if success:
                                return True, plan

                            self.backtrack_count += 1
                            print("add_attraction failed, backtrack...")

                            plan = self.trace_back(plan, current_day)
                            self.attractions_visiting.pop()
                            self.spot_type_visiting.pop()
                            self.attraction_names_visiting.pop()

                # The last event in a day: hotel or go-back

                if current_day == query["days"] - 1:  # last day
                    # go back
                    period = TimeUtils.get_time_of_day(poi_plan["back_transport"]['depature_time'])[1][0]
                    plan = self.delete_empty_period(plan, current_day)
                    if len(plan["dayInfos"]) < current_day + 1:  # if the plan does not have the last day, then add
                        plan.append({"day": current_day + 1, "scheduleDetail": [{"period": period, "detailList": []}]})
                    if period not in  [i['period'] for i in plan["dayInfos"][current_day]["scheduleDetail"]]:
                        plan["dayInfos"][current_day]["scheduleDetail"].append({"period": period, "detailList": []})
                    
                    self.search_nodes += 1
                    transports_ranking = self.innercity_transports_ranking_from_query  # inner-city transportation
                    try:
                        new_position = (poi_plan["back_transport"]["from"]["lat"], poi_plan["back_transport"]["from"]["lng"])
                    except:
                        new_position = current_position
                    for trans_type_sel in transports_ranking:
                        self.search_nodes += 1
                        transports_sel = self.collect_innercity_transport(
                            query["target_city"],
                            current_position,
                            new_position,
                            current_time,
                            trans_type_sel,
                        )
                        if not isinstance(transports_sel, list):
                            self.backtrack_count += 1
                            print("inner-city transport error, backtrack...")
                            continue

                        plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"] = self.add_intercity_transport(  # add return transportation
                            plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"],
                            poi_plan["back_transport"],
                            innercity_transports=transports_sel,
                            tickets=self.query["people_number"],
                        )

                        res_bool, res_plan = self.constraints_validation(
                            query, plan, poi_plan
                        )

                        if res_bool:
                            return True, res_plan
                        else:
                            plan = self.trace_back(plan, current_day)

                            self.backtrack_count += 1

                            print(
                                "Back-transport, but constraints_validation failed, backtrack..."
                            )
                            # return False, plan

                elif self.query["days"] > 1:
                    # go to hotel
                    hotel_sel = poi_plan["accommodation"]
                    new_position = (hotel_sel['lat'], hotel_sel['lon'])
                    if current_position == (0,0):
                        current_position = new_position
                    self.search_nodes += 1
                    transports_ranking = self.innercity_transports_ranking_from_query
                    for trans_type_sel in transports_ranking:
                        self.search_nodes += 1
                        transports_sel = self.collect_innercity_transport(
                            query["target_city"],
                            current_position,
                            new_position,
                            current_time,
                            trans_type_sel,
                        )
                        if not isinstance(transports_sel, list):
                            self.backtrack_count += 1
                            print("inner-city transport error, backtrack...")
                            continue

                        if len(transports_sel) == 0:
                            arrived_time = current_time
                        else:
                            arrived_time = transports_sel[-1]["end_time"]

                        plan = self.add_accommodation(
                            current_plan=plan,
                            hotel_sel=hotel_sel,
                            current_day=current_day,
                            arrived_time=arrived_time,
                            required_rooms=self.required_rooms,
                            transports_sel=transports_sel,
                        )

                        new_time = "00:00"
                        new_position = new_position

                        success, plan = self.dfs_poi(
                            query,
                            poi_plan,
                            plan,
                            new_time,
                            new_position,
                            current_day + 1,
                        )

                        if success:
                            return True, plan
                        else:
                            self.backtrack_count += 1
                            print("Try the go back hotel, failed, backtrack...")
                            plan = self.trace_back(plan, current_day)

                            # return False, plan
            else:
                # raise Exception("Not Implemented.")
                print("incorrect poi type: {}".format(poi_type))
                continue

            candidates_type.remove(poi_type)
            print("try another poi type, backtrack...")

        return False, plan

 
    def collect_innercity_transport(self, city:str, start:tuple, end:tuple, start_time:str, trans_type:str)->List[Dict]:
        """
        collect the inner-city transportation information
        
        Args:
            city: city name
            start: start point coordinates (lat, lon)
            end: end point coordinates (lat, lon)
            start_time: departure time
            trans_type: transportation type
            
        Returns:
            List[Dict]: transportation information list
        """
        # if the start and end are the same, return an empty list
        if start == end:
            return []
            
        # calculate the distance
        distance = self.poi_analyzer.calculate_distance(start[0], start[1], end[0], end[1])
        
        # speed mapping (km/h)
        speed_mapping = {
            "metro": 35, 
            "taxi": 60, 
            "bus": 35, 
            "train": 35, 
            "ship": 35, 
            "drive": 60, 
            "walking": 5,
            "walk": 5
        }
        
        # price mapping (yuan/km)
        price_mapping = {
            "metro": 2.5,  # metro base price
            "taxi": 3.0,   # taxi base price
            "bus": 2.0,    # bus base price
            "train": 2.5,  # train base price
            "ship": 5.0,   # ship base price
            "drive": 0.8,  # drive oil price
            "walking": 0,  # walking free
            "walk": 0      # walk free
        }
        
        # determine the transportation mode
        if trans_type:
            # if the transportation type is specified, use the specified type
            mode = trans_type
            speed = speed_mapping.get(mode, 35)
        else:
            # automatically select the appropriate transportation mode according to the distance
            if distance < 2:
                mode = "walking"
                speed = 5
            elif distance < 20:
                mode = "metro"
                speed = 35
            else:
                mode = "taxi"
                speed = 60
        
        # calculate the time (hour)
        time = distance / speed
        new_time = TimeUtils.time_operation_add(start_time, time)
        if not new_time:
            new_time = "24:00"
        
        # calculate the base price
        base_price = price_mapping.get(mode, 2.0)
        
        # get the number of people
        people_number = 1
        if self.query and "people_number" in self.query:
            people_number = self.query["people_number"]
        else:
            people_number = 1
        
        # calculate the final price according to the transportation mode
        if mode in ["metro", "bus", "train"]:
            # public transportation: calculate the price according to the distance, with the minimum price
            price = max(base_price, distance * base_price * 0.5)
            tickets = people_number
            cost = price * tickets
            
            info = [{
                "mode": mode,
                "distance": round(distance, 2),
                "time": round(time, 2),
                "price": round(price, 2),
                "cost": round(cost, 2),
                "tickets": tickets,
                "start_time": start_time,
                "end_time": new_time,
                "start_location": start,
                "end_location": end
            }]
            
        elif mode == "taxi":
            if distance <= 3:
                price = 13  # starting price
            else:
                price = 13 + (distance - 3) * base_price
            
            # calculate the number of vehicles needed (each vehicle can hold up to 4 people)
            cars = int((people_number - 1) / 4) + 1
            cost = price * cars
            
            info = [{
                "mode": mode,
                "distance": round(distance, 2),
                "time": round(time, 2),
                "price": round(price, 2),
                "cost": round(cost, 2),
                "cars": cars,
                "start_time": start_time,
                "end_time": new_time,
                "start_location": start,
                "end_location": end
            }]
            
        elif mode in ["walking", "walk"]:
            # walking: free
            info = [{
                "mode": "walking",
                "distance": round(distance, 2),
                "time": round(time, 2),
                "price": 0,
                "cost": 0,
                "start_time": start_time,
                "end_time": new_time,
                "start_location": start,
                "end_location": end
            }]
            
        elif mode == "drive":
            # drive: only calculate the oil price
            price = distance * base_price
            cost = price
            
            info = [{
                "mode": mode,
                "distance": round(distance, 2),
                "time": round(time, 2),
                "price": round(price, 2),
                "cost": round(cost, 2),
                "start_time": start_time,
                "end_time": new_time,
                "start_location": start,
                "end_location": end
            }]
            
        else:
            # other transportation modes (ship, etc.)
            price = distance * base_price
            cost = price * people_number
            
            info = [{
                "mode": mode,
                "distance": round(distance, 2),
                "time": round(time, 2),
                "price": round(price, 2),
                "cost": round(cost, 2),
                "tickets": people_number,
                "start_time": start_time,
                "end_time": new_time,
                "start_location": start,
                "end_location": end
            }]
        
        return info # example output: [{"mode":"metro","distance":10.0,"time":0.29,"price":2.5,"cost":25.0,"tickets":1,"start_time":"07:00","start_location":(39.90872,116.39749),"end_location":(39.90872,116.39749)}]

    def collect_intercity_transport(self, source_city:str, target_city:str, trans_type:str)->List[Dict]:
        """
        according to the start city, target city and transportation type, return the corresponding transportation information
        """
        trans_type_mapping = {  
            "bus": "B",
            "train": "T",
            "flight": "F",
            "drive": "SC",
            "driving": "D",
            "ship": "S",
        }
        trans_info = []
        trans_type = trans_type
        # traverse self.memory["transports"], find the transportation_id corresponding to the start city and target city
        for transport in self.poi_analyzer.transportation_info:
            from_city = transport["key"].split("->")[0]
            to_city = transport["key"].split("->")[1]
            if from_city == source_city and to_city == target_city:
                trans_info.append(transport)

        return trans_info
    
    def trace_back(self, plan, current_day):
        plan = self.delete_empty_period(plan, current_day)

        if len(plan["dayInfos"][current_day]["scheduleDetail"])>0:  # delete empty activity
            plan["dayInfos"][current_day]["scheduleDetail"][-1]["detailList"].pop()

        return plan
    
    def delete_empty_period(self, plan, current_day):
        for period in plan["dayInfos"][current_day]["scheduleDetail"]:  # delete empty period
            if len(period["detailList"]) == 0:
                plan["dayInfos"][current_day]["scheduleDetail"].remove(period)
        return plan
    
    def update_transport_pool(self, query, transport_info):
        transport_pool = json.loads(query['transport_pool'])
        if transport_info['key'] not in transport_pool:
            transport_pool[transport_info['key']] = []
        transport_pool[transport_info['key']].append(transport_info)
        query['transport_pool'] = json.dumps(transport_pool)
        return query



