import math as m
import yaml
import logging

from OSMPythonTools.overpass import Overpass, overpassQueryBuilder
from OSMPythonTools.cachingStrategy import CachingStrategy, JSON
from pywikibot import ItemPage, Site
from pywikibot import config
config.put_throttle = 0
config.maxlag = 0

from structs.preferences import Preferences, Preference
from structs.landmark import Landmark
from .take_most_important import take_most_important
import constants




class LandmarkManager:

    logger = logging.getLogger(__name__)

    radius_close_to: int    # radius in meters
    church_coeff: float     # coeff to adjsut score of churches
    nature_coeff: float       # coeff to adjust score of parks
    overall_coeff: float        # coeff to adjust weight of tags
    N_important: int        # number of important landmarks to consider


    def __init__(self) -> None:

        with constants.AMENITY_SELECTORS_PATH.open('r') as f:
            self.amenity_selectors = yaml.safe_load(f)

        with constants.LANDMARK_PARAMETERS_PATH.open('r') as f:
            parameters = yaml.safe_load(f)
            self.max_bbox_side = parameters['city_bbox_side']
            self.radius_close_to = parameters['radius_close_to']
            self.church_coeff = parameters['church_coeff']
            self.nature_coeff = parameters['nature_coeff']
            self.overall_coeff = parameters['overall_coeff']
            self.tag_exponent = parameters['tag_exponent']
            self.image_bonus = parameters['image_bonus']
            self.wikipedia_bonus = parameters['wikipedia_bonus']
            self.viewpoint_bonus = parameters['viewpoint_bonus']
            self.pay_bonus = parameters['pay_bonus']
            self.N_important = parameters['N_important']
            
        with constants.OPTIMIZER_PARAMETERS_PATH.open('r') as f:
            parameters = yaml.safe_load(f)
            self.walking_speed = parameters['average_walking_speed']
            self.detour_factor = parameters['detour_factor']

        self.overpass = Overpass()
        CachingStrategy.use(JSON, cacheDir=constants.OSM_CACHE_DIR)


    def generate_landmarks_list(self, center_coordinates: tuple[float, float], preferences: Preferences) -> tuple[list[Landmark], list[Landmark]]:
        """
        Generate and prioritize a list of landmarks based on user preferences.

        This method fetches landmarks from various categories (sightseeing, nature, shopping) based on the user's preferences
        and current location. It scores and corrects these landmarks, removes duplicates, and then selects the most important
        landmarks based on a predefined criterion.

        Parameters:
        center_coordinates (tuple[float, float]): The latitude and longitude of the center location around which to search.
        preferences (Preferences): The user's preference settings that influence the landmark selection.

        Returns:
            tuple[list[Landmark], list[Landmark]]:
                - A list of all existing landmarks.
                - A list of the most important landmarks based on the user's preferences.
        """

        max_walk_dist = (preferences.max_time_minute/2)/60*self.walking_speed*1000/self.detour_factor
        reachable_bbox_side = min(max_walk_dist, self.max_bbox_side)

        L = []
        bbox = self.create_bbox(center_coordinates, reachable_bbox_side)
        # list for sightseeing
        if preferences.sightseeing.score != 0:
            score_function = lambda score: int(score*10*preferences.sightseeing.score/5)   # self.count_elements_close_to(loc) +
            L1 = self.fetch_landmarks(bbox, self.amenity_selectors['sightseeing'], preferences.sightseeing.type, score_function)
            L += L1

        # list for nature
        if preferences.nature.score != 0:
            score_function = lambda score: int(score*10*self.nature_coeff*preferences.nature.score/5)   # self.count_elements_close_to(loc) +
            L2 = self.fetch_landmarks(bbox, self.amenity_selectors['nature'], preferences.nature.type, score_function)
            L += L2

        # list for shopping
        if preferences.shopping.score != 0:
            score_function = lambda score: int(score*10*preferences.shopping.score/5)   # self.count_elements_close_to(loc) +
            L3 = self.fetch_landmarks(bbox, self.amenity_selectors['shopping'], preferences.shopping.type, score_function)
            L += L3


        L = self.remove_duplicates(L)
        # self.correct_score(L, preferences)

        L_constrained = take_most_important(L, self.N_important)
        self.logger.info(f'Generated {len(L)} landmarks around {center_coordinates}, and constrained to {len(L_constrained)} most important ones.')

        return L, L_constrained


    def remove_duplicates(self, landmarks: list[Landmark]) -> list[Landmark]:
        """
        Removes duplicate landmarks based on their names from the given list. Only retains the landmark with highest score

        Parameters:
        landmarks (list[Landmark]): A list of Landmark objects.

        Returns:
        list[Landmark]: A list of unique Landmark objects based on their names.
        """

        L_clean = []
        names = []

        for landmark in landmarks:
            if landmark.name in names: 
                continue  
            else:
                names.append(landmark.name)
                L_clean.append(landmark)
        
        return L_clean
        

    def correct_score(self, landmarks: list[Landmark], preferences: Preferences) -> None:
        """
        Adjust the attractiveness score of each landmark in the list based on user preferences.

        This method updates the attractiveness of each landmark by scaling it according to the user's preference score.
        The score adjustment is computed using a simple linear transformation based on the preference score.

        Args:
            landmarks (list[Landmark]): A list of landmarks whose scores need to be corrected.
            preferences (Preferences): The user's preference settings that influence the attractiveness score adjustment.
        """

        score_dict = {
            preferences.sightseeing.type: preferences.sightseeing.score,
            preferences.nature.type: preferences.nature.score,
            preferences.shopping.type: preferences.shopping.score
        }
        for landmark in landmarks:
            landmark.attractiveness = int(landmark.attractiveness * score_dict[landmark.type] / 5)        


    def count_elements_close_to(self, coordinates: tuple[float, float]) -> int:
        """
        Count the number of OpenStreetMap elements (nodes, ways, relations) within a specified radius of the given location.

        This function constructs a bounding box around the specified coordinates based on the radius. It then queries
        OpenStreetMap data to count the number of elements within that bounding box.

        Args:
            coordinates (tuple[float, float]): The latitude and longitude of the location to search around.

        Returns:
            int: The number of elements (nodes, ways, relations) within the specified radius. Returns 0 if no elements
                are found or if an error occurs during the query.
        """
        
        lat = coordinates[0]
        lon = coordinates[1]

        radius = self.radius_close_to

        alpha = (180*radius) / (6371000*m.pi)
        bbox = {'latLower':lat-alpha,'lonLower':lon-alpha,'latHigher':lat+alpha,'lonHigher': lon+alpha}

        # Build the query to find elements within the radius
        radius_query = overpassQueryBuilder(
            bbox=[bbox['latLower'],
            bbox['lonLower'],
            bbox['latHigher'],
            bbox['lonHigher']],
            elementType=['node', 'way', 'relation']
        )

        try: 
            radius_result = self.overpass.query(radius_query)
            N_elem = radius_result.countWays() + radius_result.countRelations()
            self.logger.debug(f"There are {N_elem} ways/relations within 50m")
            if N_elem is None:
                return 0
            return N_elem
        except:
            return 0


    def create_bbox(self, coordinates: tuple[float, float], reachable_bbox_side: int) -> tuple[float, float, float, float]:
        """
        Create a bounding box around the given coordinates.

        Args:
            coordinates (tuple[float, float]): The latitude and longitude of the center of the bounding box.
            reachable_bbox_side (int): The side length of the bounding box in meters.

        Returns:
            tuple[float, float, float, float]: The minimum latitude, minimum longitude, maximum latitude, and maximum longitude
                                                defining the bounding box.
        """
        
        lat = coordinates[0]
        lon = coordinates[1]

        # Half the side length in km (since it's a square bbox)
        half_side_length_km = reachable_bbox_side / 2 / 1000

        # Convert distance to degrees
        lat_diff = half_side_length_km / 111  # 1 degree latitude is approximately 111 km
        lon_diff = half_side_length_km / (111 * m.cos(m.radians(lat)))  # Adjust for longitude based on latitude

        # Calculate bbox
        min_lat = lat - lat_diff
        max_lat = lat + lat_diff
        min_lon = lon - lon_diff
        max_lon = lon + lon_diff

        return min_lat, min_lon, max_lat, max_lon


    def fetch_landmarks(self, bbox: tuple, amenity_selector: dict, landmarktype: str, score_function: callable) -> list[Landmark]:
        """
        Fetches landmarks of a specified type from OpenStreetMap (OSM) within a bounding box centered on given coordinates.

        Args:
            bbox (tuple[float, float, float, float]): The bounding box coordinates (min_lat, min_lon, max_lat, max_lon).
            amenity_selector (dict): The Overpass API query selector for the desired landmark type. 
            landmarktype (str): The type of the landmark (e.g., 'sightseeing', 'nature', 'shopping').
            score_function (callable): The function to compute the score of the landmark based on its attributes.

        Returns:
            list[Landmark]: A list of Landmark objects that were fetched and filtered based on the provided criteria.

        Notes:
            - Landmarks are fetched using Overpass API queries.
            - Selectors are translated from the dictionary to the Overpass query format. (e.g., 'amenity'='place_of_worship')
            - Landmarks are filtered based on various conditions including tags and type.
            - Scores are assigned to landmarks based on their attributes and surrounding elements.
        """
        return_list = []

        # caution, when applying a list of selectors, overpass will search for elements that match ALL selectors simultaneously
        # we need to split the selectors into separate queries and merge the results
        for sel in dict_to_selector_list(amenity_selector):
            self.logger.debug(f"Current selector: {sel}")
            query = overpassQueryBuilder(
                bbox = bbox,
                elementType = ['way', 'relation'],
                selector = sel,
                # conditions = [],
                includeCenter = True,
                out = 'body'
                )

            try:
                result = self.overpass.query(query)
            except Exception as e:
                self.logger.error(f"Error fetching landmarks: {e}")
                return
            
            for elem in result.elements():

                name = elem.tag('name')                             # Add name
                location = (elem.centerLat(), elem.centerLon())     # Add coordinates (lat, lon)

                # TODO: exclude these from the get go
                # skip if unprecise location
                if name is None or location[0] is None:
                    continue

                # skip if unused
                # if 'disused:leisure' in elem.tags().keys():
                #     continue
                
                # skip if part of another building
                if 'building:part' in elem.tags().keys() and elem.tag('building:part') == 'yes':
                    continue
            
                osm_type = elem.type()              # Add type: 'way' or 'relation'
                osm_id = elem.id()                  # Add OSM id 
                elem_type = landmarktype            # Add the landmark type as 'sightseeing, 
                n_tags = len(elem.tags().keys())    # Add number of tags
                score = n_tags**self.tag_exponent   # Add score
                website_url = None
                wikpedia_url = None
                image_url = None
                name_en = None

                # remove specific tags
                skip = False
                for tag in elem.tags().keys():
                    if "pay" in tag:
                        score += self.pay_bonus             # discard payment options for tags

                    if "disused" in tag:
                        skip = True             # skip disused amenities
                        break

                    if "wiki" in tag:
                        score += self.wikipedia_bonus             # wikipedia entries count more
                        
                    # if tag == "wikidata":
                    #     Q = elem.tag('wikidata')
                    #     site = Site("wikidata", "wikidata")
                    #     item = ItemPage(site, Q)
                    #     item.get()
                    #     n_languages = len(item.labels)
                    #     n_tags += n_languages/10

                    if "viewpoint" in tag:
                        score += self.viewpoint_bonus
                        duration = 10

                    if "image" in tag:
                        score += self.image_bonus

                    if elem_type != "nature":
                        if "leisure" in tag and elem.tag('leisure') == "park":
                            elem_type = "nature"

                    if landmarktype != "shopping":
                        if "shop" in tag:
                            skip = True
                            break

                        if tag == "building" and elem.tag('building') in ['retail', 'supermarket', 'parking']:
                            skip = True
                            break
                    
                    # Get additional information
                    # if tag == 'wikipedia' :
                    #     wikpedia_url = elem.tag('wikipedia')
                    if tag in ['website', 'contact:website'] :
                        website_url = elem.tag(tag)
                    if tag == 'image' :
                        image_url = elem.tag('image')
                    if tag =='name:en' :
                        name_en = elem.tag('name:en')

                if skip:
                    continue

                score = score_function(score)
                if "place_of_worship" in elem.tags().values() :
                    score = int(score*self.church_coeff)
                    duration = 15
                
                elif "museum" in elem.tags().values() :
                    score = int(score*self.church_coeff)
                    duration = 60
                
                else : 
                    duration = 5

                # Generate the landmark and append it to the list
                landmark = Landmark(
                    name=name,
                    type=elem_type,
                    location=location,
                    osm_type=osm_type,
                    osm_id=osm_id,
                    attractiveness=score,
                    must_do=False,
                    n_tags=int(n_tags),
                    duration = duration, 
                    name_en=name_en,
                    image_url=image_url,
                    # wikipedia_url=wikpedia_url,
                    website_url=website_url
                )
                return_list.append(landmark)
        
        self.logger.debug(f"Fetched {len(return_list)} landmarks of type {landmarktype} in {bbox}")

        return return_list



def dict_to_selector_list(d: dict) -> list:
    """
    Convert a dictionary of key-value pairs to a list of Overpass query strings.

    Args:
        d (dict): A dictionary of key-value pairs representing the selector.

    Returns:
        list: A list of strings representing the Overpass query selectors.
    """
    return_list = []
    for key, value in d.items():
        if type(value) == list:
            val = '|'.join(value)
            return_list.append(f'{key}~"{val}"')
        elif type(value) == str and len(value) == 0:
            return_list.append(f'{key}')
        else:
            return_list.append(f'{key}={value}')
    return return_list
