# FlightMonitor/map/map_utils.py

import logging
import math
from typing import Tuple, Optional, List, Set, Dict, Any

try:
    import pyproj
    PYPROJ_MODULE_LOCALLY_AVAILABLE = True
except ImportError:
    pyproj = None # type: ignore
    PYPROJ_MODULE_LOCALLY_AVAILABLE = False
    logging.warning("MapUtils: 'pyproj' not found. Geographic calculations will be impaired.")

try:
    import mercantile
    MERCANTILE_MODULE_LOCALLY_AVAILABLE = True
except ImportError:
    mercantile = None # type: ignore
    MERCANTILE_MODULE_LOCALLY_AVAILABLE = False
    logging.warning("MapUtils: 'mercantile' not found. Tile operations will fail.")

try:
    from ..utils.logger import get_logger
    logger = get_logger(__name__) 
except ImportError: 
    logger = logging.getLogger(__name__)
    if not logger.hasHandlers():
        logging.basicConfig(level=logging.DEBUG) 
    logger.warning("MapUtils using fallback standard Python logger.")

try:
    from . import map_constants
except ImportError:
    logger.error("MapUtils: map_constants not found. Using mock constants.")
    class MockMapConstants:
        COORDINATE_DECIMAL_PLACES = 4
        DMS_DEGREE_SYMBOL = " deg" 
        DMS_MINUTE_SYMBOL = "' "
        DMS_SECOND_SYMBOL = "''"
        MIN_ZOOM_LEVEL = 0
        DEFAULT_MAX_ZOOM_FALLBACK = 20
    map_constants = MockMapConstants() # type: ignore


class MapCalculationError(Exception):
    """Custom exception for map calculations."""
    pass


def _is_valid_bbox_dict(bbox_dict: Dict[str, Any]) -> bool:
    if not isinstance(bbox_dict, dict):
        # logger.debug("BBox validation failed: not dict.") # Troppo verboso per un check comune
        return False
    required_keys = ["lat_min", "lon_min", "lat_max", "lon_max"]
    if not all(key in bbox_dict for key in required_keys):
        # logger.debug(f"BBox validation failed: missing keys {required_keys}.")
        return False

    try:
        lat_min = float(bbox_dict["lat_min"])
        lon_min = float(bbox_dict["lon_min"])
        lat_max = float(bbox_dict["lat_max"])
        lon_max = float(bbox_dict["lon_max"])

        if not (-90.0 <= lat_min <= 90.0 and -90.0 <= lat_max <= 90.0 and
                -180.0 <= lon_min <= 180.0 and -180.0 <= lon_max <= 180.0):
            # logger.debug(f"BBox validation failed: coordinates out of range {bbox_dict}.")
            return False
        if lat_min >= lat_max :
            # logger.debug(f"BBox validation failed: lat_min ({lat_min}) >= lat_max ({lat_max}).")
            return False
        if lon_min >= lon_max: # Stretta per OpenSky, ma potrebbe essere valida per mappe che wrappano
             # logger.debug(f"BBox validation failed: lon_min ({lon_min}) >= lon_max ({lon_max}).")
             return False 
        if not all(math.isfinite(coord) for coord in [lat_min, lon_min, lat_max, lon_max]):
            # logger.debug(f"BBox validation failed: non-finite values {bbox_dict}.")
            return False
        return True
    except (ValueError, TypeError):
        # logger.debug(f"BBox validation failed: invalid types/values {bbox_dict}. Error: {e}")
        return False
    except Exception as e_val: # Dovrebbe essere raro
        logger.error(f"Unexpected error validating BBox: {bbox_dict}. Error: {e_val}", exc_info=True)
        return False

def get_bounding_box_from_center_size(
    center_latitude_deg: float, center_longitude_deg: float, area_size_km: float
) -> Optional[Tuple[float, float, float, float]]:
    if not PYPROJ_MODULE_LOCALLY_AVAILABLE or pyproj is None:
        logger.error("'pyproj' required for BBox from center/size. Not available.")
        return None

    if not (isinstance(area_size_km, (int, float)) and area_size_km > 0):
        logger.error(f"Invalid area_size_km: {area_size_km}.")
        return None
    if not (isinstance(center_latitude_deg, (int, float)) and -90.0 <= center_latitude_deg <= 90.0):
        logger.error(f"Invalid center_latitude_deg: {center_latitude_deg}.")
        return None
    if not (isinstance(center_longitude_deg, (int, float)) and -180.0 <= center_longitude_deg <= 180.0):
        logger.error(f"Invalid center_longitude_deg: {center_longitude_deg}.")
        return None

    try:
        geodetic_calculator = pyproj.Geod(ellps="WGS84")
        half_side_m = (area_size_km / 2.0) * 1000.0

        _, north_lat, _ = geodetic_calculator.fwd(center_longitude_deg, center_latitude_deg, 0.0, half_side_m)
        _, south_lat, _ = geodetic_calculator.fwd(center_longitude_deg, center_latitude_deg, 180.0, half_side_m)
        east_lon, _, _ = geodetic_calculator.fwd(center_longitude_deg, center_latitude_deg, 90.0, half_side_m)
        west_lon, _, _ = geodetic_calculator.fwd(center_longitude_deg, center_latitude_deg, 270.0, half_side_m)
        
        north_lat = min(90.0, max(-90.0, north_lat))
        south_lat = min(90.0, max(-90.0, south_lat))
        east_lon = (east_lon + 180) % 360 - 180
        west_lon = (west_lon + 180) % 360 - 180
        if east_lon == -180.0 and center_longitude_deg > 0 : east_lon = 180.0
        if west_lon == 180.0 and center_longitude_deg < 0 : west_lon = -180.0

        if not all(math.isfinite(coord) for coord in [west_lon, south_lat, east_lon, north_lat]):
            logger.error("Calculated BBox from center/size contains non-finite values.")
            return None
        
        if west_lon > east_lon and area_size_km < 20000: 
            logger.warning(f"Calculated BBox has west_lon ({west_lon}) > east_lon ({east_lon}) for non-global size. Swapping.")
            west_lon, east_lon = east_lon, west_lon

        return (west_lon, south_lat, east_lon, north_lat)

    except Exception as e_bbox_calc:
        logger.exception(f"Error calculating BBox from center/size: {e_bbox_calc}")
        return None


def get_tile_ranges_for_bbox(
    bounding_box_deg: Tuple[float, float, float, float], 
    zoom_level: int,
) -> Optional[Tuple[Tuple[int, int], Tuple[int, int]]]:
    if not MERCANTILE_MODULE_LOCALLY_AVAILABLE or mercantile is None:
        logger.error("'mercantile' required for tile ranges, but not available.")
        return None

    west_lon, south_lat, east_lon, north_lat = bounding_box_deg
    
    clamped_south_lat = max(-85.05112877, min(85.05112877, south_lat))
    clamped_north_lat = max(-85.05112877, min(85.05112877, north_lat))

    if clamped_south_lat >= clamped_north_lat:
        logger.warning(f"Clamped latitude range invalid for tile calculation: S={clamped_south_lat:.4f}, N={clamped_north_lat:.4f}.")
        center_lat = (south_lat + north_lat) / 2.0
        center_lon = (west_lon + east_lon) / 2.0 
        if west_lon > east_lon: center_lon = (west_lon + (east_lon + 360.0)) / 2.0;_ = 0;_ = (0 if center_lon <= 180 else (center_lon := center_lon - 360.0))
        try:
            tile = mercantile.tile(center_lon, center_lat, zoom_level)
            return ((tile.x, tile.x), (tile.y, tile.y))
        except Exception as e_single_tile:
            logger.error(f"Could not get single tile for invalid lat range: {e_single_tile}")
            return None

    if not (isinstance(zoom_level, int) and 0 <= zoom_level <= 25):
        logger.error(f"Invalid zoom level for tile calculation: {zoom_level}.")
        return None

    try:
        tiles_in_bbox_generator = mercantile.tiles(west_lon, clamped_south_lat, east_lon, clamped_north_lat, zooms=[zoom_level])
        list_of_tiles = list(tiles_in_bbox_generator)

        if not list_of_tiles:
            logger.warning(f"No tiles found by mercantile for BBox {bounding_box_deg} at zoom {zoom_level}. Attempting center tile.")
            center_lon = (west_lon + east_lon) / 2.0
            if west_lon > east_lon: center_lon = (west_lon + (east_lon + 360.0)) / 2.0; _ = (0 if center_lon <= 180 else (center_lon := center_lon - 360.0))
            center_lat = (clamped_south_lat + clamped_north_lat) / 2.0
            try:
                center_tile = mercantile.tile(center_lon, center_lat, zoom_level)
                min_x, max_x = center_tile.x, center_tile.x
                min_y, max_y = center_tile.y, center_tile.y
            except Exception as e_center_tile:
                logger.error(f"Fallback failed: Error getting tile for BBox center: {e_center_tile}", exc_info=True)
                return None
        else:
            x_coords = [tile.x for tile in list_of_tiles]
            y_coords = [tile.y for tile in list_of_tiles]
            min_x, max_x = min(x_coords), max(x_coords)
            min_y, max_y = min(y_coords), max(y_coords)
            
        return ((min_x, max_x), (min_y, max_y))

    except Exception as e_tile_range_calc:
        logger.exception(f"Error calculating tile ranges for BBox {bounding_box_deg} at zoom {zoom_level}: {e_tile_range_calc}")
        return None


def calculate_meters_per_pixel(
    latitude_degrees: float, zoom_level: int, tile_pixel_size: int = 256
) -> Optional[float]:
    try:
        if not (isinstance(latitude_degrees, (int, float)) and -90.0 <= latitude_degrees <= 90.0): return None
        if not (isinstance(zoom_level, int) and 0 <= zoom_level <= 25): return None
        if not (isinstance(tile_pixel_size, int) and tile_pixel_size > 0): return None

        EARTH_CIRCUMFERENCE_METERS = 40075016.686 
        clamped_lat = max(-85.05112878, min(85.05112878, latitude_degrees))
        lat_radians = math.radians(clamped_lat)
        
        denominator = tile_pixel_size * (2**zoom_level)
        if abs(denominator) < 1e-9: return None

        resolution_m_px = (EARTH_CIRCUMFERENCE_METERS * math.cos(lat_radians)) / denominator
        
        if not math.isfinite(resolution_m_px) or resolution_m_px <= 0: return None
        return resolution_m_px

    except Exception as e_mpp_calc:
        logger.exception(f"Error calculating meters per pixel: {e_mpp_calc}")
        return None


def calculate_geographic_bbox_size_km(
    bounding_box_deg: Tuple[float, float, float, float], 
) -> Optional[Tuple[float, float]]:
    if not PYPROJ_MODULE_LOCALLY_AVAILABLE or pyproj is None:
        logger.error("'pyproj' required for BBox size calculation, but not available.")
        return None

    west_lon, south_lat, east_lon, north_lat = bounding_box_deg

    if not (isinstance(west_lon, (int, float)) and isinstance(south_lat, (int, float)) and
            isinstance(east_lon, (int, float)) and isinstance(north_lat, (int, float))):
        logger.warning(f"Invalid coordinate types for BBox size calculation: {bounding_box_deg}.")
        return None
    if not (-90.0 <= south_lat <= north_lat <= 90.0):
        logger.warning(f"Invalid latitude range for BBox size calculation: S={south_lat}, N={north_lat}.")
        south_lat = max(-90.0, south_lat); north_lat = min(90.0, north_lat)
        if south_lat >= north_lat : return None

    try:
        geod = pyproj.Geod(ellps="WGS84")
        avg_lon_for_height = (west_lon + east_lon) / 2.0
        if west_lon > east_lon: 
            avg_lon_for_height = (west_lon + (east_lon + 360.0)) / 2.0
            if avg_lon_for_height > 180.0: avg_lon_for_height -= 360.0
        
        _, _, height_m = geod.inv(avg_lon_for_height, south_lat, avg_lon_for_height, north_lat)
        height_m = abs(height_m)
        avg_lat_for_width = (south_lat + north_lat) / 2.0
        _, _, width_m = geod.inv(west_lon, avg_lat_for_width, east_lon, avg_lat_for_width)
        width_m = abs(width_m)

        approx_width_km = width_m / 1000.0
        approx_height_km = height_m / 1000.0

        if approx_width_km < 1e-6: approx_width_km = 0.001 
        if approx_height_km < 1e-6: approx_height_km = 0.001

        if not all(math.isfinite(val) for val in [approx_width_km, approx_height_km]):
            logger.warning(f"Calculated BBox size has non-finite values: W={approx_width_km} km, H={approx_height_km} km.")
            return None
            
        return (approx_width_km, approx_height_km)
    except Exception as e_size_calc:
        logger.exception(f"Error calculating BBox size for {bounding_box_deg}: {e_size_calc}")
        return None


def get_hgt_tile_geographic_bounds(
    lat_coord: int, lon_coord: int
) -> Optional[Tuple[float, float, float, float]]:
    if not (isinstance(lat_coord, int) and isinstance(lon_coord, int)): return None
    south_lat = float(lat_coord); west_lon = float(lon_coord)
    north_lat = south_lat + 1.0; east_lon = west_lon + 1.0
    south_lat = max(-90.0, min(south_lat, 89.0)); north_lat = max(-89.0, min(north_lat, 90.0))
    west_lon = max(-180.0, min(west_lon, 179.0)); east_lon = max(-179.0, min(east_lon, 180.0))
    if south_lat >= north_lat or west_lon >= east_lon: return None
    return (west_lon, south_lat, east_lon, north_lat)


def calculate_zoom_level_for_geographic_size(
    latitude_degrees: float, 
    geographic_span_meters: float, 
    target_pixel_span: int, 
    tile_pixel_size: int = 256
) -> Optional[int]:
    if not (isinstance(latitude_degrees, (int, float)) and -90.0 <= latitude_degrees <= 90.0): return None
    if not (isinstance(geographic_span_meters, (int, float)) and geographic_span_meters > 0): return None
    if not (isinstance(target_pixel_span, int) and target_pixel_span > 0): return None
    if not (isinstance(tile_pixel_size, int) and tile_pixel_size > 0): return None

    try:
        required_resolution_m_px = geographic_span_meters / target_pixel_span
        if required_resolution_m_px <= 1e-9 or not math.isfinite(required_resolution_m_px):
            logger.warning(f"Calculated non-positive/non-finite required resolution ({required_resolution_m_px}). Cannot determine zoom.")
            return None

        EARTH_CIRCUMFERENCE_METERS = 40075016.686
        clamped_lat = max(-85.05112878, min(85.05112878, latitude_degrees))
        lat_radians = math.radians(clamped_lat)
        cos_lat = math.cos(lat_radians)

        if abs(cos_lat) < 1e-9: 
            logger.warning(f"Latitude {latitude_degrees} too close to pole for reliable zoom calculation based on span.")
            return map_constants.MIN_ZOOM_LEVEL 

        term_for_log = (EARTH_CIRCUMFERENCE_METERS * cos_lat) / (tile_pixel_size * required_resolution_m_px)
        if term_for_log <= 1e-9: 
            logger.warning(f"Calculated non-positive term for log2 ({term_for_log:.2e}) in zoom calculation.")
            return map_constants.MIN_ZOOM_LEVEL 

        precise_zoom = math.log2(term_for_log)
        
        # MODIFIED: Usare round() per uno zoom più aderente. Il margine sarà gestito altrove.
        integer_zoom = int(round(precise_zoom)) 

        min_zoom = getattr(map_constants, "MIN_ZOOM_LEVEL", 0)
        # Usa un max_zoom più realistico per i servizi di tile comuni come OSM
        max_zoom = getattr(map_constants, "DEFAULT_MAX_ZOOM_FALLBACK", 19) # OSM va fino a 19-20
        
        clamped_zoom = max(min_zoom, min(integer_zoom, max_zoom))
        
        logger.debug(f"CalcZoomForGeoSize: precise={precise_zoom:.2f}, rounded={integer_zoom}, clamped={clamped_zoom} for span {geographic_span_meters}m in {target_pixel_span}px at lat {latitude_degrees:.2f}")
        return clamped_zoom

    except Exception as e_zoom_calc:
        logger.exception(f"Error calculating zoom level for geographic size: {e_zoom_calc}")
        return None


def deg_to_dms_string(degree_value: float, coord_type: str) -> str:
    if not isinstance(degree_value, (int, float)) or not math.isfinite(degree_value):
        return "N/A"
    if coord_type.lower() not in ("lat", "lon"):
        return "N/A (Invalid Type)"

    val_for_suffix = degree_value
    if coord_type.lower() == "lat":
        if not (-90.0 <= degree_value <= 90.0): val_for_suffix = max(-90.0, min(90.0, degree_value))
    elif coord_type.lower() == "lon":
        val_for_suffix = (degree_value + 180) % 360 - 180
        if val_for_suffix == -180.0 and degree_value != -180.0: val_for_suffix = 180.0

    abs_deg_val = abs(degree_value)
    degrees = int(abs_deg_val)
    minutes_decimal = (abs_deg_val - degrees) * 60.0
    minutes = int(minutes_decimal)
    seconds = (minutes_decimal - minutes) * 60.0
    
    seconds = round(seconds, 2)
    if seconds >= 60.0: seconds = 0.0; minutes += 1
    if minutes >= 60: minutes = 0; degrees +=1
    
    if coord_type.lower() == "lat" and degrees > 90: degrees = 90
    elif coord_type.lower() == "lon" and degrees > 180: degrees = 180
        
    suffix = ""
    if coord_type.lower() == "lat": suffix = "N" if val_for_suffix >= 0 else "S"
    elif coord_type.lower() == "lon":
        if abs(val_for_suffix) < 1e-9 : suffix = "" 
        else: suffix = "E" if val_for_suffix >= 0 else "W"

    try:
        dms_str = (f"{degrees}{map_constants.DMS_DEGREE_SYMBOL} "
                   f"{minutes:02d}{map_constants.DMS_MINUTE_SYMBOL} "
                   f"{seconds:05.2f}{map_constants.DMS_SECOND_SYMBOL}")
    except AttributeError: 
        dms_str = f"{degrees}deg {minutes:02d}m {seconds:05.2f}s"
    
    return f"{dms_str} {suffix}".strip()


def get_combined_geographic_bounds_from_tile_info_list(
    tile_info_list: List[Dict],
) -> Optional[Tuple[float, float, float, float]]:
    if not tile_info_list: return None
    min_lon_overall, min_lat_overall, max_lon_overall, max_lat_overall = 180.0, 90.0, -180.0, -90.0
    initialized = False
    for tile_info in tile_info_list:
        lat_c = tile_info.get("latitude_coord")
        lon_c = tile_info.get("longitude_coord")
        if lat_c is None or lon_c is None: continue
        try:
            tile_bounds_wesn = get_hgt_tile_geographic_bounds(int(lat_c), int(lon_c))
            if tile_bounds_wesn:
                w, s, e, n = tile_bounds_wesn
                if not initialized:
                    min_lon_overall, min_lat_overall, max_lon_overall, max_lat_overall = w, s, e, n
                    initialized = True
                else:
                    min_lon_overall = min(min_lon_overall, w)
                    min_lat_overall = min(min_lat_overall, s)
                    max_lon_overall = max(max_lon_overall, e)
                    max_lat_overall = max(max_lat_overall, n)
        except (ValueError, TypeError): continue
        except Exception: continue # Catch any other error during tile bound processing
    if not initialized: return None
    # Basic check for validity, though individual HGT bounds should be valid
    if min_lat_overall >= max_lat_overall or min_lon_overall >= max_lon_overall : return None 
    return (min_lon_overall, min_lat_overall, max_lon_overall, max_lat_overall)


def calculate_geographic_bbox_from_pixel_size_and_zoom(
    center_latitude_deg: float,
    center_longitude_deg: float,
    target_pixel_width: int,
    target_pixel_height: int,
    zoom_level: int,
    tile_pixel_size: int = 256
) -> Optional[Tuple[float, float, float, float]]: 
    if not MERCANTILE_MODULE_LOCALLY_AVAILABLE or mercantile is None:
        logger.error("Mercantile missing, cannot calculate BBox from pixel size.")
        return None
    
    if not (isinstance(center_latitude_deg, (int, float)) and -90.0 <= center_latitude_deg <= 90.0 and
            isinstance(center_longitude_deg, (int, float)) and -180.0 <= center_longitude_deg <= 180.0 and
            isinstance(target_pixel_width, int) and target_pixel_width > 0 and
            isinstance(target_pixel_height, int) and target_pixel_height > 0 and
            isinstance(zoom_level, int) and 0 <= zoom_level <= 25 and
            isinstance(tile_pixel_size, int) and tile_pixel_size > 0):
        logger.error(f"Invalid parameters for calculate_geographic_bbox_from_pixel_size_and_zoom: center=({center_latitude_deg},{center_longitude_deg}), target_px=({target_pixel_width}x{target_pixel_height}), zoom={zoom_level}")
        return None

    try:
        center_merc_x, center_merc_y = mercantile.xy(center_longitude_deg, center_latitude_deg, truncate=False)
        resolution_m_px = calculate_meters_per_pixel(center_latitude_deg, zoom_level, tile_pixel_size)
        if resolution_m_px is None: return None

        half_width_meters = (target_pixel_width / 2.0) * resolution_m_px
        half_height_meters = (target_pixel_height / 2.0) * resolution_m_px

        ul_merc_x = center_merc_x - half_width_meters
        ul_merc_y = center_merc_y + half_height_meters
        lr_merc_x = center_merc_x + half_width_meters
        lr_merc_y = center_merc_y - half_height_meters

        west_lon, north_lat = mercantile.lnglat(ul_merc_x, ul_merc_y, truncate=False)
        east_lon, south_lat = mercantile.lnglat(lr_merc_x, lr_merc_y, truncate=False)
        
        MAX_MERC_LAT = 85.05112878 
        north_lat = max(-MAX_MERC_LAT, min(MAX_MERC_LAT, north_lat))
        south_lat = max(-MAX_MERC_LAT, min(MAX_MERC_LAT, south_lat))
        
        west_lon = (west_lon + 180) % 360 - 180
        east_lon = (east_lon + 180) % 360 - 180
        if west_lon == 180 and center_longitude_deg < 0: west_lon = -180.0 
        if east_lon == -180 and center_longitude_deg > 0: east_lon = 180.0

        if abs(west_lon - east_lon) < 1e-9 or abs(south_lat - north_lat) < 1e-9 : return None
        if not all(math.isfinite(coord) for coord in [west_lon, south_lat, east_lon, north_lat]): return None

        return (west_lon, south_lat, east_lon, north_lat)
    except Exception as e_bbox_calc:
        logger.exception(f"Error calculating geo BBox from pixel size/zoom: {e_bbox_calc}")
        return None

def _pixel_to_geo(
    canvas_x: int,
    canvas_y: int,
    current_map_geo_bounds: Optional[Tuple[float, float, float, float]], 
    current_stitched_map_pixel_shape: Optional[Tuple[int, int]], 
) -> Tuple[Optional[float], Optional[float]]: 
    if not MERCANTILE_MODULE_LOCALLY_AVAILABLE or mercantile is None:
        logger.error("_pixel_to_geo: Mercantile missing or not available.")
        return None, None
    if current_map_geo_bounds is None or len(current_map_geo_bounds) != 4:
        logger.warning("_pixel_to_geo: Map geo bounds missing or invalid.")
        return None, None
    if current_stitched_map_pixel_shape is None or len(current_stitched_map_pixel_shape) != 2:
        logger.warning("_pixel_to_geo: Map pixel shape missing or invalid.")
        return None, None

    img_w, img_h = current_stitched_map_pixel_shape
    if img_w <= 0 or img_h <= 0:
        logger.warning(f"_pixel_to_geo: Map pixel dims invalid ({img_w}x{img_h}).")
        return None, None

    map_west_lon, map_south_lat, map_east_lon, map_north_lat = current_map_geo_bounds

    try:
        map_ul_merc_x, map_ul_merc_y = mercantile.xy(map_west_lon, map_north_lat, truncate=False)
        map_lr_merc_x, map_lr_merc_y = mercantile.xy(map_east_lon, map_south_lat, truncate=False)

        total_map_width_merc = map_lr_merc_x - map_ul_merc_x 
        total_map_height_merc = map_ul_merc_y - map_lr_merc_y 

        if map_east_lon < map_west_lon: 
            map_east_lon_equiv = map_east_lon + 360.0
            temp_lr_merc_x, _ = mercantile.xy(map_east_lon_equiv, map_south_lat, truncate=False)
            total_map_width_merc = temp_lr_merc_x - map_ul_merc_x

        if abs(total_map_width_merc) < 1e-9 or abs(total_map_height_merc) < 1e-9:
            logger.warning(f"_pixel_to_geo: Map mercator dims zero/near-zero ({total_map_width_merc:.2e}x{total_map_height_merc:.2e}).")
            return None, None

        relative_x_on_map_pixels = canvas_x / img_w
        relative_y_on_map_pixels = canvas_y / img_h
        
        target_merc_x = map_ul_merc_x + (relative_x_on_map_pixels * total_map_width_merc)
        target_merc_y = map_ul_merc_y - (relative_y_on_map_pixels * total_map_height_merc)

        clicked_lon, clicked_lat = mercantile.lnglat(target_merc_x, target_merc_y, truncate=False)
        
        MAX_MERCATOR_LATITUDE = 85.05112878 
        clicked_lat = max(-MAX_MERCATOR_LATITUDE, min(MAX_MERCATOR_LATITUDE, clicked_lat))
        clicked_lon = (clicked_lon + 180) % 360 - 180
        if clicked_lon == -180.0 and target_merc_x > 0: clicked_lon = 180.0

        return clicked_lon, clicked_lat
    except Exception as e_pixel_to_geo:
        logger.error(f"Error converting pixel ({canvas_x}, {canvas_y}) to geo. Error: {e_pixel_to_geo}", exc_info=True)
        return None, None