# FlightMonitor/map/map_canvas_manager.py

import tkinter as tk
import math
import time
from typing import Optional, Tuple, List, Dict, Any
from collections import deque
import queue # Still needed for queue.Empty exception
import threading # Still needed for threading.Lock, threading.Event
from . import map_utils 
import copy

try:
    from PIL import Image, ImageTk, ImageDraw, ImageFont
    PIL_IMAGE_LIB_AVAILABLE = True
except ImportError:
    Image, ImageTk, ImageDraw, ImageFont = None, None, None, None # type: ignore
    PIL_IMAGE_LIB_AVAILABLE = False
    import logging 
    logging.error("MapCanvasManager: Pillow (Image, ImageTk, ImageDraw, ImageFont) not found. Map disabled.")

from .map_utils import PYPROJ_MODULE_LOCALLY_AVAILABLE, MERCANTILE_MODULE_LOCALLY_AVAILABLE

from . import map_constants
from ..data import config as app_config
from ..data.common_models import CanonicalFlightState
from .map_services import BaseMapService, OpenStreetMapService
from .map_tile_manager import MapTileManager
from .map_utils import (
    get_tile_ranges_for_bbox,
    calculate_geographic_bbox_size_km,
    calculate_geographic_bbox_from_pixel_size_and_zoom,
    _is_valid_bbox_dict,
    _pixel_to_geo,
    calculate_meters_per_pixel,
    calculate_zoom_level_for_geographic_size,
    get_bounding_box_from_center_size,
)
from . import map_drawing

# MODIFIED: Import MapRenderManager
# WHY: To delegate the management of the background rendering worker thread.
# HOW: Added import statement.
from .map_render_manager import MapRenderManager, RENDER_REQUEST_TYPE_MAP, RENDER_REQUEST_TYPE_SHUTDOWN


try:
    from ..utils.logger import get_logger
    logger = get_logger(__name__)
except ImportError:
    logger = logging.getLogger(__name__)
    logger.warning("MapCanvasManager using fallback standard Python logger.")


CANVAS_SIZE_HARD_FALLBACK_PX = getattr(app_config, "DEFAULT_CANVAS_WIDTH", 800)
MAP_TILE_CACHE_DIR_HARD_FALLBACK = getattr(
    app_config, "MAP_TILE_CACHE_DIR", "flightmonitor_tile_cache_fallback"
)
RESIZE_DEBOUNCE_DELAY_MS = 250
PAN_STEP_FRACTION = 0.25
DEFAULT_MAX_TRACK_POINTS = getattr(
    app_config, "DEFAULT_TRACK_HISTORY_POINTS", 20
)
DEFAULT_MAX_TRACK_AGE_SECONDS = 300
# MODIFIED: Removed MAP_WORKER_QUEUE_TIMEOUT_S, RENDER_REQUEST_TYPE_MAP, RENDER_REQUEST_TYPE_SHUTDOWN
# WHY: These are now constants within MapRenderManager.
# HOW: Deleted these lines.
# MAP_WORKER_QUEUE_TIMEOUT_S = 0.1
# RENDER_REQUEST_TYPE_MAP = "render_map"
# RENDER_REQUEST_TYPE_SHUTDOWN = "shutdown_worker"

GUI_RESULT_POLL_INTERVAL_MS = 50

RENDER_OVERSIZE_FACTOR = 1.5 
CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT = 0.10


class MapCanvasManager:

    def __init__(
        self,
        app_controller: Any,
        tk_canvas: tk.Canvas,
        initial_bbox_dict: Optional[Dict[str, float]],
        is_detail_map: bool = False
    ):
        self.is_detail_map = is_detail_map
        self.log_prefix = f"MCM (detail={self.is_detail_map})" 
        logger.info(f">>> {self.log_prefix} __init__ STARTING <<<")

        if (
            not PIL_IMAGE_LIB_AVAILABLE
            or not MERCANTILE_MODULE_LOCALLY_AVAILABLE
            or map_utils.mercantile is None 
        ):
            critical_msg = f"{self.log_prefix}: Critical dependencies missing: Pillow or Mercantile. Map disabled."
            logger.critical(critical_msg)
            if app_controller and hasattr(app_controller, "main_window") and app_controller.main_window:
                 if hasattr(app_controller.main_window, "show_error_message"):
                    try:
                        app_controller.main_window.show_error_message(
                            "Map Initialization Error", critical_msg.replace(f"{self.log_prefix}: ", "")
                        )
                    except Exception: pass
            raise ImportError(critical_msg)

        self.app_controller = app_controller
        self.canvas = tk_canvas

        self.canvas_width = self.canvas.winfo_width()
        if self.canvas_width <= 1: self.canvas_width = CANVAS_SIZE_HARD_FALLBACK_PX
        self.canvas_height = self.canvas.winfo_height()
        if self.canvas_height <= 1: self.canvas_height = getattr(app_config, "DEFAULT_CANVAS_HEIGHT", 600)
        
        logger.info(f"{self.log_prefix} __init__: Canvas dims {self.canvas_width}x{self.canvas_height}")

        self._current_center_lat_gui: Optional[float] = None
        self._current_center_lon_gui: Optional[float] = None
        self._current_zoom_gui: int = map_constants.DEFAULT_INITIAL_ZOOM
        self._current_map_geo_bounds_gui: Optional[Tuple[float, float, float, float]] = None
        self._target_bbox_input_gui: Optional[Dict[str, float]] = None
        self._map_photo_image: Optional[ImageTk.PhotoImage] = None
        self._canvas_image_id: Optional[int] = None
        self._placeholder_text_id: Optional[int] = None
        self.map_service: BaseMapService = OpenStreetMapService()
        self.tile_manager: MapTileManager = MapTileManager(
            map_service=self.map_service,
            cache_root_directory=MAP_TILE_CACHE_DIR_HARD_FALLBACK,
            tile_pixel_size=self.map_service.tile_size,
        )
        logger.info(f"{self.log_prefix} __init__: MapTileManager initialized.")
        self._current_flights_to_display_gui: List[CanonicalFlightState] = []
        self.flight_tracks_gui: Dict[str, deque] = {}
        self.max_track_points: int = DEFAULT_MAX_TRACK_POINTS
        self.max_track_age_seconds: float = DEFAULT_MAX_TRACK_AGE_SECONDS
        self._resize_debounce_job_id: Optional[str] = None
        
        # MODIFIED: Instantiate MapRenderManager
        # WHY: To delegate worker thread management.
        # HOW: Created an instance, and set its render pipeline callable.
        self.map_render_manager = MapRenderManager()
        self.map_render_manager.set_render_pipeline_callable(self._execute_render_pipeline)
        # MODIFIED: Removed queue and thread related attributes, now managed by MapRenderManager
        # self._map_render_request_queue: queue.Queue = queue.Queue(maxsize=5)
        # self._map_render_result_queue: queue.Queue = queue.Queue(maxsize=5)
        # self._map_worker_stop_event: threading.Event = threading.Event()
        # self._map_worker_thread: Optional[threading.Thread] = None

        self._gui_after_id_result_processor: Optional[str] = None
        
        # MODIFIED: Removed _last_render_request_id, _expected_render_id_gui
        # WHY: These are now managed internally by MapRenderManager.
        # HOW: Deleted these attributes.
        # self._last_render_request_id: int = 0
        # self._expected_render_id_gui: int = 0
        
        self._map_data_lock: threading.Lock = threading.Lock()
        logger.info(f"{self.log_prefix} __init__: All attributes initialized.")

        # MODIFIED: Start the worker via MapRenderManager
        # WHY: Delegation of worker lifecycle.
        # HOW: Called self.map_render_manager.start_worker().
        logger.info(f"{self.log_prefix} __init__: Attempting to start map worker thread...")
        self.map_render_manager.start_worker()

        logger.info(f"{self.log_prefix} __init__: Processing initial_bbox_dict: {initial_bbox_dict}")
        if initial_bbox_dict and _is_valid_bbox_dict(initial_bbox_dict):
            self._target_bbox_input_gui = initial_bbox_dict.copy()
            logger.info(f"{self.log_prefix} __init__: Valid initial_bbox_dict provided. Requesting render for bbox.")
            self._request_map_render_for_bbox(initial_bbox_dict, preserve_current_zoom_if_possible=False)
        elif not self.is_detail_map:
            logger.warning(f"{self.log_prefix} __init__ (MAIN MAP): Invalid or no initial_bbox_dict. Using default fallback view.")
            default_bbox_cfg = {
                "lat_min": app_config.DEFAULT_BBOX_LAT_MIN, "lon_min": app_config.DEFAULT_BBOX_LON_MIN,
                "lat_max": app_config.DEFAULT_BBOX_LAT_MAX, "lon_max": app_config.DEFAULT_BBOX_LON_MAX,
            }
            if _is_valid_bbox_dict(default_bbox_cfg):
                self._target_bbox_input_gui = default_bbox_cfg.copy()
                logger.info(f"{self.log_prefix} __init__ (MAIN MAP): Using default config BBox. Requesting render.")
                self._request_map_render_for_bbox(self._target_bbox_input_gui, preserve_current_zoom_if_possible=False)
            else:
                logger.critical(f"{self.log_prefix} __init__ (MAIN MAP): Default fallback BBox from config is invalid: {default_bbox_cfg}.")
                self._target_bbox_input_gui = None
                self._current_center_lat_gui = getattr(app_config, "DEFAULT_MAP_CENTER_LAT", 45.0)
                self._current_center_lon_gui = getattr(app_config, "DEFAULT_MAP_CENTER_LON", 9.0)
                self._current_zoom_gui = map_constants.DEFAULT_INITIAL_ZOOM
                logger.info(f"{self.log_prefix} __init__ (MAIN MAP): Using hardcoded default center/zoom. Requesting render.")
                self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui)
        else:
             logger.info(f"{self.log_prefix} __init__ (DETAIL MAP): No initial_bbox_dict. Map will be blank until track data is provided.")
             self._display_placeholder_text("Detail Map\nAwaiting flight data...")

        self._setup_event_bindings()
        logger.info(f"{self.log_prefix} __init__: Event bindings set up.")

        logger.info(f"{self.log_prefix} __init__: Attempting to start GUI result processing...")
        self._start_gui_result_processing()

        logger.info(f">>> {self.log_prefix} __init__ FINISHED <<<")

    # MODIFIED: Removed _start_map_worker_thread
    # WHY: Worker thread management is now delegated to MapRenderManager.
    # HOW: Deleted this method.
    # def _start_map_worker_thread(self):
    #     # ... (content moved to MapRenderManager) ...
    #     pass

    # MODIFIED: _map_render_worker_target moved to MapRenderManager
    # WHY: This method contains the core logic of the rendering worker thread.
    # HOW: Deleted this method.
    # def _map_render_worker_target(self):
    #     # ... (content moved to MapRenderManager) ...
    #     pass

    def _start_gui_result_processing(self):
        if self._gui_after_id_result_processor:
            try:
                if self.canvas.winfo_exists():
                    self.canvas.after_cancel(self._gui_after_id_result_processor)
            except Exception as e_cancel:
                logger.warning(f"{self.log_prefix} Error cancelling previous result processor: {e_cancel}")
        if self.canvas.winfo_exists():
            self._gui_after_id_result_processor = self.canvas.after(
                GUI_RESULT_POLL_INTERVAL_MS, self._process_map_render_results
            )
            logger.debug(f"{self.log_prefix} GUI result processing scheduled.")
        else:
            logger.warning(f"{self.log_prefix} Canvas does not exist, cannot schedule GUI result processing.")

    def _execute_render_pipeline(
        self,
        center_lat_of_view: float,    
        center_lon_of_view: float,    
        zoom_level_for_view: int,   
        final_canvas_w_px: int,       
        final_canvas_h_px: int,       
        target_bbox_to_center_and_draw: Optional[Dict[str, float]], 
        draw_target_bbox_overlay_flag: bool,
        current_flights_to_display: List[CanonicalFlightState],
        flight_tracks: Dict[str, deque],
        max_track_points_config: int,
    ) -> Tuple[
        Optional[ImageTk.PhotoImage],
        Optional[Tuple[float, float, float, float]],
        Optional[str],
    ]:
        log_prefix_pipeline = f"{self.log_prefix} WorkerPipeline_Crop"
        logger.info(f"{log_prefix_pipeline}: STARTING. ViewCenter=({center_lat_of_view:.4f},{center_lon_of_view:.4f}), ViewZoom={zoom_level_for_view}, FinalCanvas=({final_canvas_w_px}x{final_canvas_h_px}), DrawTargetBBoxFlag={draw_target_bbox_overlay_flag}")
        
        if target_bbox_to_center_and_draw:
            logger.debug(f"{log_prefix_pipeline}: Target BBox for potential centering/drawing: {target_bbox_to_center_and_draw}")

        if not (PIL_IMAGE_LIB_AVAILABLE and MERCANTILE_MODULE_LOCALLY_AVAILABLE and map_utils.mercantile and Image and ImageDraw and ImageTk):
            return None, None, f"{log_prefix_pipeline}: Core dependencies missing."
        if final_canvas_w_px <= 0 or final_canvas_h_px <= 0:
            return None, None, f"{log_prefix_pipeline}: Invalid final canvas dimensions."

        render_pixel_w_oversized = int(final_canvas_w_px * RENDER_OVERSIZE_FACTOR)
        render_pixel_h_oversized = int(final_canvas_h_px * RENDER_OVERSIZE_FACTOR)
        logger.debug(f"{log_prefix_pipeline}: Oversized render pixel dimensions for tile fetching: {render_pixel_w_oversized}x{render_pixel_h_oversized}")

        oversized_geo_bbox = calculate_geographic_bbox_from_pixel_size_and_zoom(
            center_lat_of_view, center_lon_of_view,
            render_pixel_w_oversized, render_pixel_h_oversized,
            zoom_level_for_view, self.tile_manager.tile_size
        )
        if not oversized_geo_bbox:
            return None, None, f"{log_prefix_pipeline}: Failed to calculate oversized_geo_bbox for rendering."
        logger.debug(f"{log_prefix_pipeline}: Oversized GEO BBox for tile stitching: {oversized_geo_bbox}")
        
        tile_xy_ranges = get_tile_ranges_for_bbox(oversized_geo_bbox, zoom_level_for_view)
        if not tile_xy_ranges:
            err_msg = f"{log_prefix_pipeline}: Failed to get tile ranges for oversized_geo_bbox {oversized_geo_bbox} at Z{zoom_level_for_view}."
            logger.error(err_msg)
            try:
                placeholder_img = Image.new("RGB", (final_canvas_w_px, final_canvas_h_px), map_constants.DEFAULT_PLACEHOLDER_COLOR_RGB) # type: ignore
                draw = ImageDraw.Draw(placeholder_img)
                map_drawing._draw_text_on_placeholder(draw, placeholder_img.size, err_msg.replace(f"{log_prefix_pipeline}: ", ""))
                if ImageTk: return ImageTk.PhotoImage(placeholder_img), None, err_msg
            except Exception as e_ph: return None, None, f"{log_prefix_pipeline}: Tile range error AND placeholder creation failed: {e_ph}"
            return None, None, err_msg


        stitched_map_oversized_pil = self.tile_manager.stitch_map_image(zoom_level_for_view, tile_xy_ranges[0], tile_xy_ranges[1])
        if not stitched_map_oversized_pil:
            return None, None, f"{log_prefix_pipeline}: Failed to stitch oversized map image."

        actual_stitched_oversized_geo_bounds = self.tile_manager._get_bounds_for_tile_range(zoom_level_for_view, tile_xy_ranges)
        if not actual_stitched_oversized_geo_bounds:
            actual_stitched_oversized_geo_bounds = oversized_geo_bbox 
        
        logger.debug(f"{log_prefix_pipeline}: Stitched OVERSIZED map PIL size: {stitched_map_oversized_pil.size}, GeoBounds: {actual_stitched_oversized_geo_bounds}")
        
        target_center_x_on_oversized_px: float
        target_center_y_on_oversized_px: float

        center_of_view_px_tuple = map_drawing._geo_to_pixel_on_unscaled_map(
            center_lat_of_view, center_lon_of_view,
            actual_stitched_oversized_geo_bounds, stitched_map_oversized_pil.size
        )

        if center_of_view_px_tuple:
            target_center_x_on_oversized_px, target_center_y_on_oversized_px = center_of_view_px_tuple
        else: 
            logger.warning(f"{log_prefix_pipeline}: Could not convert view center to pixel. Cropping from center of oversized image.")
            target_center_x_on_oversized_px = stitched_map_oversized_pil.width / 2.0
            target_center_y_on_oversized_px = stitched_map_oversized_pil.height / 2.0
        
        logger.debug(f"{log_prefix_pipeline}: Effective center for crop (px on oversized): ({target_center_x_on_oversized_px:.1f}, {target_center_y_on_oversized_px:.1f})")

        crop_x0 = round(target_center_x_on_oversized_px - (final_canvas_w_px / 2.0))
        crop_y0 = round(target_center_y_on_oversized_px - (final_canvas_h_px / 2.0))
        crop_x1 = crop_x0 + final_canvas_w_px
        crop_y1 = crop_y0 + final_canvas_h_px
        
        calculated_crop_box_pil = (crop_x0, crop_y0, crop_x1, crop_y1) 
        logger.info(f"{log_prefix_pipeline}: Initial CROP BOX (on stitched_map_oversized {stitched_map_oversized_pil.size}): {calculated_crop_box_pil} for final canvas {final_canvas_w_px}x{final_canvas_h_px}")

        actual_crop_x0 = max(0, calculated_crop_box_pil[0])
        actual_crop_y0 = max(0, calculated_crop_box_pil[1])
        actual_crop_x1 = min(stitched_map_oversized_pil.width, calculated_crop_box_pil[2])
        actual_crop_y1 = min(stitched_map_oversized_pil.height, calculated_crop_box_pil[3])
        
        adjusted_crop_box = (actual_crop_x0, actual_crop_y0, actual_crop_x1, actual_crop_y1)
        cropped_image_segment = stitched_map_oversized_pil.crop(adjusted_crop_box)
        logger.info(f"{log_prefix_pipeline}: Adjusted crop box: {adjusted_crop_box}, Cropped segment size: {cropped_image_segment.size}")

        final_image_for_canvas_pil = Image.new("RGB", (final_canvas_w_px, final_canvas_h_px), map_constants.DEFAULT_PLACEHOLDER_COLOR_RGB) # type: ignore
        
        paste_x = (final_canvas_w_px - cropped_image_segment.width) // 2
        paste_y = (final_canvas_h_px - cropped_image_segment.height) // 2
        final_image_for_canvas_pil.paste(cropped_image_segment, (paste_x, paste_y))
        
        final_map_geo_bounds_for_overlays = calculate_geographic_bbox_from_pixel_size_and_zoom(
            center_lat_of_view, center_lon_of_view,
            final_canvas_w_px, final_canvas_h_px,
            zoom_level_for_view,
            self.tile_manager.tile_size
        )
        if not final_map_geo_bounds_for_overlays:
             logger.error(f"{log_prefix_pipeline}: Critical error calculating final_map_geo_bounds_for_overlays. Using stitched oversized bounds as fallback.")
             final_map_geo_bounds_for_overlays = actual_stitched_oversized_geo_bounds # Fallback
        
        logger.info(f"{log_prefix_pipeline}: Final image size after crop/pad: {final_image_for_canvas_pil.size}, GeoBounds for overlays: {final_map_geo_bounds_for_overlays}")
        
        if final_image_for_canvas_pil.mode != "RGBA":
            image_to_draw_on = final_image_for_canvas_pil.convert("RGBA")
        else:
            image_to_draw_on = final_image_for_canvas_pil.copy()
        
        img_shape_for_drawing = image_to_draw_on.size
        draw = ImageDraw.Draw(image_to_draw_on)

        if draw_target_bbox_overlay_flag and target_bbox_to_center_and_draw and _is_valid_bbox_dict(target_bbox_to_center_and_draw):
            bbox_wesn = (target_bbox_to_center_and_draw["lon_min"], target_bbox_to_center_and_draw["lat_min"], target_bbox_to_center_and_draw["lon_max"], target_bbox_to_center_and_draw["lat_max"])
            try:
                map_drawing.draw_area_bounding_box(
                    image_to_draw_on, bbox_wesn, final_map_geo_bounds_for_overlays, img_shape_for_drawing,
                    color=map_constants.AREA_BOUNDARY_COLOR, thickness=map_constants.AREA_BOUNDARY_THICKNESS_PX
                )
                logger.debug(f"{log_prefix_pipeline}: Drew target BBox overlay on final image because flag was True.")
            except Exception as e_bbox_draw_final:
                logger.error(f"{log_prefix_pipeline}: Error drawing target BBox overlay on final image: {e_bbox_draw_final}", exc_info=False)
        elif target_bbox_to_center_and_draw:
             logger.debug(f"{log_prefix_pipeline}: Skipped drawing target BBox overlay because flag was False (is_detail_map={self.is_detail_map}).")

        if current_flights_to_display:
            font_size = map_constants.DEM_TILE_LABEL_BASE_FONT_SIZE + (zoom_level_for_view - map_constants.DEM_TILE_LABEL_BASE_ZOOM)
            label_font = map_drawing._load_label_font(font_size)
            for flight in current_flights_to_display:
                if flight.latitude is not None and flight.longitude is not None:
                    pixel_coords_f = map_drawing._geo_to_pixel_on_unscaled_map(
                        flight.latitude, flight.longitude, final_map_geo_bounds_for_overlays, img_shape_for_drawing)
                    if pixel_coords_f:
                        current_flight_track_deque = flight_tracks.get(flight.icao24)
                        map_drawing._draw_single_flight(
                            draw, pixel_coords_f, flight, label_font,
                            track_deque=current_flight_track_deque,
                            current_map_geo_bounds=final_map_geo_bounds_for_overlays,
                            current_stitched_map_pixel_shape=img_shape_for_drawing)
            logger.debug(f"{log_prefix_pipeline}: Drew {len(current_flights_to_display)} flight overlays on final image.")

        try:
            if ImageTk:
                final_photo_image = ImageTk.PhotoImage(image_to_draw_on)
                return final_photo_image, final_map_geo_bounds_for_overlays, None 
            else:
                return None, final_map_geo_bounds_for_overlays, f"{log_prefix_pipeline}: ImageTk module not available."
        except Exception as e_photo_pipe_final:
            return None, final_map_geo_bounds_for_overlays, f"{log_prefix_pipeline}: Failed to create final PhotoImage: {e_photo_pipe_final}"


    def _process_map_render_results(self):
        # MODIFIED: Get results from MapRenderManager
        # WHY: MapRenderManager now manages the result queue.
        # HOW: Call self.map_render_manager.get_render_result().
        if not self.canvas.winfo_exists():
            logger.info(f"{self.log_prefix} Canvas destroyed, stopping map render result processing.")
            self._gui_after_id_result_processor = None
            return

        processed_one = False
        try:
            # MODIFIED: Loop to get results from MapRenderManager
            # WHY: Decoupling queue access.
            # HOW: Replaced direct queue.empty() and queue.get_nowait() with MapRenderManager methods.
            while True: # Loop until no more results or an error occurs
                result_data = self.map_render_manager.get_render_result()
                if result_data is None: # No more results in queue
                    break
                
                processed_one = True
                request_id = result_data.get("request_id")
                photo_image = result_data.get("photo_image")
                rendered_map_bounds = result_data.get("map_geo_bounds")
                error_message = result_data.get("error")

                # MODIFIED: Get expected GUI ID from MapRenderManager
                # WHY: MapRenderManager is the source of truth for request IDs.
                # HOW: Call self.map_render_manager.get_expected_gui_render_id().
                expected_gui_id = self.map_render_manager.get_expected_gui_render_id()
                logger.info(f"{self.log_prefix} GUI ResultsProcessor: Dequeued result for ReqID {request_id}. Error: '{error_message}', Img Valid: {photo_image is not None}, Expected GUI ID: {expected_gui_id}")

                if request_id < expected_gui_id:
                    logger.warning(f"{self.log_prefix} GUI ResultsProcessor: Discarding STALE map render result ID {request_id} (expected >= {expected_gui_id}).")
                    continue # Continue to next result

                if error_message:
                    logger.error(f"{self.log_prefix} GUI ResultsProcessor: Received error from Worker (ReqID {request_id}): {error_message}")
                    self._display_placeholder_text(f"Map Error (Worker):\n{error_message[:100]}")
                elif photo_image and ImageTk and isinstance(photo_image, ImageTk.PhotoImage):
                    logger.info(f"{self.log_prefix} GUI ResultsProcessor: Applying new map image from worker for ReqID {request_id}.")
                    self._clear_canvas_display_elements()
                    self._map_photo_image = photo_image
                    self._canvas_image_id = self.canvas.create_image(0, 0, anchor=tk.NW, image=self._map_photo_image)
                    self._current_map_geo_bounds_gui = rendered_map_bounds
                else:
                    logger.warning(f"{self.log_prefix} GUI ResultsProcessor: Received invalid/empty result from worker for ReqID {request_id}. Error: '{error_message}'")
                    self._display_placeholder_text(f"Map Update Failed\n(No Image Data for ReqID {request_id})")
                
                if not self.is_detail_map:
                    if self.app_controller and hasattr(self.app_controller, "update_general_map_info"):
                        logger.debug(f"{self.log_prefix} GUI ResultsProcessor (MAIN MAP): Requesting controller to update general map info after ReqID {request_id}.")
                        self.app_controller.update_general_map_info()
        
        except Exception as e: # Catch any unexpected error in result processing loop
            logger.exception(f"{self.log_prefix} GUI ResultsProcessor: Error processing map render results: {e}")
        
        if self.canvas.winfo_exists():
            self._gui_after_id_result_processor = self.canvas.after(GUI_RESULT_POLL_INTERVAL_MS, self._process_map_render_results)

    def _request_map_render(
        self,
        center_lat: float,
        center_lon: float,
        zoom_level: int,
    ):
        logger.debug(f"{self.log_prefix} GUI _request_map_render: Input center=({center_lat:.4f},{center_lon:.4f}), zoom={zoom_level}")
        
        if not self.map_render_manager.is_worker_alive():
            logger.warning(f"{self.log_prefix} Map worker not running. Cannot queue render request.")
            self._display_placeholder_text("Map Worker Offline")
            return

        with self._map_data_lock:
            # Passa sempre la BBox target memorizzata, se esiste
            target_bbox_for_render = self._target_bbox_input_gui.copy() if self._target_bbox_input_gui else None
            
            request_payload = {
                "type": RENDER_REQUEST_TYPE_MAP,
                "center_lat": center_lat, "center_lon": center_lon, "zoom": zoom_level,
                "canvas_width": self.canvas_width, "canvas_height": self.canvas_height,
                "target_bbox": target_bbox_for_render, # Usa la bbox memorizzata
                "draw_target_bbox_overlay": not self.is_detail_map,
                "flights": copy.deepcopy(self._current_flights_to_display_gui),
                "tracks": copy.deepcopy(self.flight_tracks_gui),
                "max_track_points": self.max_track_points,
            }
            current_request_id = self.map_render_manager.put_render_request(request_payload)
            if current_request_id is None:
                logger.warning(f"{self.log_prefix} Map render request ID was dropped by MapRenderManager.")
                self._display_placeholder_text("Map Busy / Request Queue Full.\nPlease Try Action Again.")
                return

            self.map_render_manager.set_expected_gui_render_id(current_request_id)
        
        self._clear_canvas_display_elements()
        loading_text = f"Loading Map...\nZ{zoom_level} @ ({center_lat:.2f}, {center_lon:.2f})\nReqID: {current_request_id}"
        self._display_placeholder_text(loading_text)
        
        self._current_center_lat_gui = center_lat
        self._current_center_lon_gui = center_lon
        self._current_zoom_gui = zoom_level


    def _setup_event_bindings(self):
        self.canvas.bind("<Configure>", self._on_canvas_resize)
        self.canvas.bind("<ButtonPress-1>", self._on_left_button_press)
        self.canvas.bind("<ButtonRelease-1>", self._on_left_button_release)
        self.canvas.bind("<ButtonPress-3>", self._on_right_click)
        self._drag_start_x_canvas: Optional[int] = None
        self._drag_start_y_canvas: Optional[int] = None
        self._is_left_button_pressed: bool = False
        logger.debug(f"{self.log_prefix} Event bindings set up.")

    def _on_canvas_resize(self, event: tk.Event):
        new_width, new_height = event.width, event.height
        if (new_width > 1 and new_height > 1 and
            (self.canvas_width != new_width or self.canvas_height != new_height)):
            logger.info(f"{self.log_prefix} _on_canvas_resize: Canvas dimensions changed from {self.canvas_width}x{self.canvas_height} to {new_width}x{new_height}. Debouncing redraw.")
            if self._resize_debounce_job_id:
                try:
                    if self.canvas.winfo_exists(): self.canvas.after_cancel(self._resize_debounce_job_id)
                except Exception: pass
            if self.canvas.winfo_exists():
                self._resize_debounce_job_id = self.canvas.after(RESIZE_DEBOUNCE_DELAY_MS, self._perform_resize_redraw, new_width, new_height)

    def _perform_resize_redraw(self, width: int, height: int):
        self._resize_debounce_job_id = None
        if not self.canvas.winfo_exists():
            logger.warning(f"{self.log_prefix} _perform_resize_redraw: Canvas does not exist. Aborting.")
            return
        logger.info(f"{self.log_prefix} _perform_resize_redraw: Executing debounced resize to {width}x{height}. Requesting new render.")
        self.canvas_width, self.canvas_height = width, height
        if (self._current_center_lat_gui is not None and
            self._current_center_lon_gui is not None and
            self._current_zoom_gui is not None):
            self._request_map_render(
                self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui,
                ensure_bbox_is_covered_dict=self._target_bbox_input_gui
            )
        else:
            logger.warning(f"{self.log_prefix} _perform_resize_redraw: Cannot redraw on resize - current map center/zoom not set.")
            self._display_placeholder_text("Map State Error\n(Cannot redraw on resize)")

    def _on_left_button_press(self, event: tk.Event):
        if not self.canvas.winfo_exists(): return
        logger.debug(f"{self.log_prefix} _on_left_button_press: CanvasX={event.x}, CanvasY={event.y}")
        self._drag_start_x_canvas, self._drag_start_y_canvas = event.x, event.y
        self._is_left_button_pressed = True

    def _on_left_button_release(self, event: tk.Event):
        if not self.canvas.winfo_exists(): return
        logger.debug(f"{self.log_prefix} Left Button Release: CanvasX={event.x}, CanvasY={event.y}")

        if not self._is_left_button_pressed:
            self._drag_start_x_canvas, self._drag_start_y_canvas = None, None
            return
        self._is_left_button_pressed = False

        clicked_flight_icao: Optional[str] = None
        if self._drag_start_x_canvas is not None and self._drag_start_y_canvas is not None:
            drag_threshold_px = 10
            dx = abs(event.x - self._drag_start_x_canvas)
            dy = abs(event.y - self._drag_start_y_canvas)
            is_click = dx < drag_threshold_px and dy < drag_threshold_px

            if is_click:
                logger.debug(f"{self.log_prefix} Left Button Release: Detected as a map click.")
                if self._current_map_geo_bounds_gui is not None and self._map_photo_image is not None:
                    try:
                        map_pixel_shape = (self._map_photo_image.width(), self._map_photo_image.height())
                        clicked_lon, clicked_lat = map_utils._pixel_to_geo(
                            event.x, event.y, self._current_map_geo_bounds_gui, map_pixel_shape
                        )
                        if clicked_lon is not None and clicked_lat is not None:
                            logger.info(f"Map Left-Clicked ({self.log_prefix}) at Geo ({clicked_lat:.5f}, {clicked_lon:.5f})")
                            
                            if hasattr(self.app_controller, "on_map_left_click") and self.app_controller.on_map_left_click:
                                # Pass both canvas and screen coordinates
                                self.app_controller.on_map_left_click(
                                    latitude=clicked_lat, 
                                    longitude=clicked_lon, 
                                    canvas_x=event.x, 
                                    canvas_y=event.y,
                                    screen_x=event.x_root,
                                    screen_y=event.y_root
                                )
                            else:
                                logger.warning(f"AppController does not have a handler for on_map_left_click ({self.log_prefix}).")

                        else:
                            logger.warning(f"Failed to convert left click pixel ({event.x},{event.y}) to geo ({self.log_prefix}).")
                            if not self.is_detail_map and hasattr(self.app_controller, "on_map_left_click") and self.app_controller.on_map_left_click:
                                self.app_controller.on_map_left_click(
                                    latitude=None, 
                                    longitude=None, 
                                    canvas_x=event.x, 
                                    canvas_y=event.y,
                                    screen_x=event.x_root,
                                    screen_y=event.y_root
                                )

                    except Exception as e_click_proc:
                        logger.error(f"Error during left click processing ({self.log_prefix}): {e_click_proc}", exc_info=True)
                        if not self.is_detail_map and hasattr(self.app_controller, "on_map_left_click") and self.app_controller.on_map_left_click:
                             self.app_controller.on_map_left_click(
                                latitude=None, 
                                longitude=None, 
                                canvas_x=event.x, 
                                canvas_y=event.y,
                                screen_x=event.x_root,
                                screen_y=event.y_root
                            )
                else:
                    logger.warning(f"Map context missing for left click ({self.log_prefix}).")
                    if not self.is_detail_map and hasattr(self.app_controller, "on_map_left_click") and self.app_controller.on_map_left_click:
                         self.app_controller.on_map_left_click(
                            latitude=None, 
                            longitude=None, 
                            canvas_x=event.x, 
                            canvas_y=event.y,
                            screen_x=event.x_root,
                            screen_y=event.y_root
                        )
        self._drag_start_x_canvas, self._drag_start_y_canvas = None, None

    # MODIFIED: Added get_clicked_flight_icao helper method
    # WHY: This helper is used by MapCommandHandler.on_map_left_click to identify the clicked flight.
    # HOW: Implemented the logic to iterate through current flights and check proximity.
    def get_clicked_flight_icao(self, canvas_x: int, canvas_y: int) -> Optional[str]:
        """
        Identifies if a flight was clicked on the map at the given canvas coordinates.
        This method is intended to be called by MapCommandHandler.

        Args:
            canvas_x: The X coordinate on the Tkinter canvas.
            canvas_y: The Y coordinate on the Tkinter canvas.

        Returns:
            Optional[str]: The ICAO24 of the clicked flight, or None if no flight was
                           clicked within the interaction radius.
        """
        if self._current_map_geo_bounds_gui is None or self._map_photo_image is None:
            logger.warning(f"MapCanvasManager: Map context missing to identify clicked flight.")
            return None
        
        map_pixel_shape = (self._map_photo_image.width(), self._map_photo_image.height())
        # No need to convert canvas_x,y to geo here, as the flight positions are already geo.
        # We just need to convert flight geo positions to pixel and check proximity.

        min_dist_sq_to_flight = float("inf")
        flight_click_radius_px_sq = 15 * 15 # Square of 15 pixels radius for click detection
        
        clicked_flight_icao: Optional[str] = None

        with self._map_data_lock: # Ensure thread safety when accessing flight data
            flights_on_map = self._current_flights_to_display_gui
        
        if flights_on_map:
            for flight in flights_on_map:
                if flight.latitude is not None and flight.longitude is not None:
                    flight_px_coords = map_drawing._geo_to_pixel_on_unscaled_map(
                        flight.latitude, flight.longitude,
                        self._current_map_geo_bounds_gui, map_pixel_shape
                    )
                    if flight_px_coords:
                        dist_sq = (canvas_x - flight_px_coords[0])**2 + (canvas_y - flight_px_coords[1])**2
                        if dist_sq < flight_click_radius_px_sq and dist_sq < min_dist_sq_to_flight:
                            min_dist_sq_to_flight = dist_sq
                            clicked_flight_icao = flight.icao24
        
        return clicked_flight_icao


    def _on_right_click(self, event: tk.Event):
        if not self.canvas.winfo_exists(): return
        logger.debug(f"{self.log_prefix} _on_right_click: CanvasX={event.x}, CanvasY={event.y}")
        if self._current_map_geo_bounds_gui is None or self._map_photo_image is None:
            logger.warning(f"Map context missing for right click geo conversion ({self.log_prefix}).")
            return
        try:
            map_pixel_shape = (self._map_photo_image.width(), self._map_photo_image.height())
            geo_lon, geo_lat = map_utils._pixel_to_geo(
                event.x, event.y, self._current_map_geo_bounds_gui, map_pixel_shape
            )
            if geo_lon is not None and geo_lat is not None:
                logger.info(f"Map Right-Clicked ({self.log_prefix}) at Geo ({geo_lat:.5f}, {geo_lon:.5f})")
                # MODIFIED: Delegate the on_map_right_click logic to MapCommandHandler
                # WHY: MapCommandHandler is responsible for handling map interactions.
                # HOW: Call self.app_controller.map_command_handler.on_map_right_click().
                if hasattr(self.app_controller, "map_command_handler") and self.app_controller.map_command_handler:
                    self.app_controller.map_command_handler.on_map_right_click(geo_lat, geo_lon, event.x_root, event.y_root)
                else:
                    logger.warning(f"AppController's MapCommandHandler not available for right click ({self.log_prefix}).")
            else:
                logger.warning(f"Failed to convert right click pixel ({event.x},{event.y}) to geo ({self.log_prefix}).")
        except Exception as e_rclick_convert:
            logger.error(f"Error during right click geo conversion ({self.log_prefix}): {e_rclick_convert}", exc_info=True)

    def set_max_track_points(self, length: int):
        new_length = max(2, length)
        if self.max_track_points != new_length:
            logger.info(f"{self.log_prefix}: Max track points updating from {self.max_track_points} to {new_length}")
            with self._map_data_lock:
                self.max_track_points = new_length
                for icao in list(self.flight_tracks_gui.keys()):
                    track_deque = self.flight_tracks_gui.get(icao)
                    if track_deque:
                        new_deque_for_icao = deque(maxlen=self.max_track_points + 5)
                        start_index = max(0, len(track_deque) - self.max_track_points)
                        for i in range(start_index, len(track_deque)):
                            new_deque_for_icao.append(track_deque[i])
                        if not new_deque_for_icao:
                            if icao in self.flight_tracks_gui: del self.flight_tracks_gui[icao]
                        else:
                            self.flight_tracks_gui[icao] = new_deque_for_icao
            if (self._current_center_lat_gui is not None and
                self._current_center_lon_gui is not None and
                self._current_zoom_gui is not None):
                self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui)

    def set_target_bbox(self, new_bbox_dict: Optional[Dict[str, float]]):
        logger.info(f"{self.log_prefix} set_target_bbox (GUI): Received new target BBox: {new_bbox_dict}")
        
        # Se la nuova BBox è valida, la impostiamo e la mostriamo
        if new_bbox_dict and _is_valid_bbox_dict(new_bbox_dict):
            with self._map_data_lock:
                self._target_bbox_input_gui = new_bbox_dict.copy()
            logger.debug(f"{self.log_prefix} set_target_bbox: _target_bbox_input_gui updated. Requesting render.")
            
            # Richiedi il rendering e aggiorna i campi della GUI
            self._request_map_render_for_bbox(self._target_bbox_input_gui, preserve_current_zoom_if_possible=False)
            if not self.is_detail_map and hasattr(self.app_controller, "update_bbox_gui_fields"):
                self.app_controller.update_bbox_gui_fields(self._target_bbox_input_gui)
        
        # Se la nuova BBox è None, significa che vogliamo cancellare quella esistente
        else:
            with self._map_data_lock:
                self._target_bbox_input_gui = None
            logger.info(f"{self.log_prefix} set_target_bbox: Target BBox cleared.")
            
            # Richiedi un re-render della mappa senza il riquadro blu della BBox
            if (self._current_center_lat_gui is not None and
                self._current_center_lon_gui is not None and
                self._current_zoom_gui is not None):
                self._request_map_render(
                    self._current_center_lat_gui,
                    self._current_center_lon_gui,
                    self._current_zoom_gui
                )
            
            # Aggiorna i campi della GUI per mostrarli vuoti (o con default)
            if not self.is_detail_map and hasattr(self.app_controller, "update_bbox_gui_fields"):
                # Passando un dizionario vuoto, il pannello della GUI azzererà i campi.
                self.app_controller.update_bbox_gui_fields({})

        # MODIFIED: Request a render ONLY if the bounding_box changed to a valid one, or if it was cleared.
        #           Otherwise, an invalid/None bbox input should not trigger a re-render just to make it disappear.
        #           The disappearance should be driven by stop_monitoring.
        # if (self._current_center_lat_gui is not None and self._current_center_lon_gui is not None and self._current_zoom_gui is not None):
        #     self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui)
        # else: self.clear_map_display() # This could clear current flights too.

        # The rendering request should ONLY happen if the BBox was valid OR if we are explicitly stopping monitoring.
        # This function is for setting the target BBox. If it's invalid input, we don't force a render just to hide.
        # The hiding/clearing is part of the stop_live_monitoring flow.
        # So, the `_request_map_render` should ideally only be called if `new_bbox_dict` was valid.
        # The first `if` branch already handles this by calling `_request_map_render_for_bbox`.
        # This `else` block should primarily just update the GUI fields.

    def _request_map_render_for_bbox(
        self,
        target_bbox_dict: Dict[str, float], 
        preserve_current_zoom_if_possible: bool = False, 
    ):
        log_prefix_rq = f"{self.log_prefix} _req_map_for_bbox"
        logger.debug(f"{log_prefix_rq}: Target BBox to fit: {target_bbox_dict}")

        if not target_bbox_dict or not _is_valid_bbox_dict(target_bbox_dict):
            logger.warning(f"{log_prefix_rq}: Invalid target BBox. Aborting.")
            if not self.is_detail_map: self._display_placeholder_text("Invalid BBox for Map Render")
            return

        lat_min_orig, lon_min_orig, lat_max_orig, lon_max_orig = (
            target_bbox_dict["lat_min"], target_bbox_dict["lon_min"],
            target_bbox_dict["lat_max"], target_bbox_dict["lon_max"]
        )

        view_center_lat, view_center_lon = (lat_min_orig + lat_max_orig) / 2.0, (lon_min_orig + lon_max_orig) / 2.0
        if lon_min_orig > lon_max_orig: 
            view_center_lon = (lon_min_orig + (lon_max_orig + 360.0)) / 2.0
            if view_center_lon >= 180.0: view_center_lon -= 360.0
        
        zoom_to_request = self._current_zoom_gui 
        
        if not preserve_current_zoom_if_possible or zoom_to_request is None:
            # ... (la logica di calcolo dello zoom rimane identica) ...
            original_width_km, original_height_km = calculate_geographic_bbox_size_km(
                (lon_min_orig, lat_min_orig, lon_max_orig, lat_max_orig)
            ) or (None, None)

            if original_width_km is None or original_height_km is None:
                zoom_to_request = self._current_zoom_gui if self._current_zoom_gui is not None else map_constants.DEFAULT_INITIAL_ZOOM
            else:
                effective_pixel_width_for_target = self.canvas_width * (1.0 - 2 * CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT)
                effective_pixel_height_for_target = self.canvas_height * (1.0 - 2 * CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT)
                
                if self.canvas_width > 0 and self.canvas_height > 0:
                    zoom_w = calculate_zoom_level_for_geographic_size(
                        view_center_lat, original_width_km * 1000,
                        int(effective_pixel_width_for_target), 
                        self.tile_manager.tile_size
                    )
                    zoom_h = calculate_zoom_level_for_geographic_size(
                        view_center_lat, original_height_km * 1000,
                        int(effective_pixel_height_for_target), 
                        self.tile_manager.tile_size
                    )
                    
                    calculated_zoom = min(zoom_w, zoom_h) if zoom_w is not None and zoom_h is not None else map_constants.DEFAULT_INITIAL_ZOOM
                    zoom_to_request = calculated_zoom
                else:
                    zoom_to_request = self._current_zoom_gui if self._current_zoom_gui is not None else map_constants.DEFAULT_INITIAL_ZOOM
        
        max_zoom_svc = self.map_service.max_zoom if self.map_service else map_constants.DEFAULT_MAX_ZOOM_FALLBACK
        final_zoom_for_request = max(map_constants.MIN_ZOOM_LEVEL, min(zoom_to_request, max_zoom_svc))
        
        # Chiamiamo il render normale, che ora passerà automaticamente la BBox
        self._request_map_render(
            center_lat=view_center_lat, 
            center_lon=view_center_lon, 
            zoom_level=final_zoom_for_request
        )

    def _clear_canvas_display_elements(self):
        if self._canvas_image_id is not None and self.canvas.winfo_exists():
            try: self.canvas.delete(self._canvas_image_id)
            except Exception: pass
            finally: self._canvas_image_id = None
        if self._map_photo_image is not None: self._map_photo_image = None
        if self._placeholder_text_id is not None and self.canvas.winfo_exists():
            try: self.canvas.delete(self._placeholder_text_id)
            except Exception: pass
            finally: self._placeholder_text_id = None

    def _display_placeholder_text(self, text: str):
        logger.debug(f"{self.log_prefix} _display_placeholder_text: Displaying '{text[:50]}...'")
        if not self.canvas.winfo_exists():
            logger.warning(f"{self.log_prefix} _display_placeholder_text: Canvas does not exist.")
            return
        self._clear_canvas_display_elements()
        try:
            canvas_w_disp, canvas_h_disp = self.canvas.winfo_width(), self.canvas.winfo_height()
            if canvas_w_disp <= 1: canvas_w_disp = self.canvas_width
            if canvas_h_disp <= 1: canvas_h_disp = self.canvas_height
            if canvas_w_disp > 1 and canvas_h_disp > 1:
                bg_color_to_use = getattr(map_constants, "DEFAULT_PLACEHOLDER_COLOR_RGB_TK", "gray90")
                self.canvas.configure(bg=bg_color_to_use)
                self._placeholder_text_id = self.canvas.create_text(
                    canvas_w_disp / 2, canvas_h_disp / 2, text=text, fill="gray10",
                    font=("Arial", 11, "normal"), justify=tk.CENTER, width=max(100, canvas_w_disp - 40)
                )
        except tk.TclError: pass
        except Exception as e: logger.error(f"{self.log_prefix} _display_placeholder_text: Unexpected error: {e}", exc_info=True)

    def clear_map_display(self):
        logger.info(f"{self.log_prefix} clear_map_display: Clearing all map content and resetting view state.")
        self._clear_canvas_display_elements()
        self._display_placeholder_text("Map Cleared / Awaiting Action")
        with self._map_data_lock:
            self._current_flights_to_display_gui = []
            self.flight_tracks_gui.clear()
        self._current_map_geo_bounds_gui = None
        
        if (self._current_center_lat_gui is not None and
            self._current_center_lon_gui is not None and
            self._current_zoom_gui is not None):
            logger.info(f"{self.log_prefix} clear_map_display: Requesting re-render of clean base map.")
            # CORREZIONE: Chiamiamo _request_map_render senza l'argomento rimosso.
            # La funzione ora userà automaticamente self._target_bbox_input_gui se presente.
            self._request_map_render(
                self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui
            )
        elif not self.is_detail_map: 
            if self.app_controller and hasattr(self.app_controller, "update_general_map_info"):
                self.app_controller.update_general_map_info()

    def update_flights_on_map(self, flight_states: List[CanonicalFlightState]):
        logger.info(f"{self.log_prefix} update_flights_on_map (GUI): Received {len(flight_states)} flight states.")
        
        with self._map_data_lock:
            # Aggiorniamo la lista dei voli correnti da visualizzare
            self._current_flights_to_display_gui = flight_states
            
            # Se non ci sono voli, puliamo le tracce e usciamo
            if not flight_states:
                self.flight_tracks_gui.clear()
            else:
                current_time = time.time()
                active_icao_this_update = set()

                for state in flight_states:
                    icao = state.icao24
                    active_icao_this_update.add(icao)
                    
                    # --- NUOVA LOGICA DI CARICAMENTO STORICO ---
                    # Se l'aereo è nuovo in questa sessione di monitoring...
                    if icao not in self.flight_tracks_gui:
                        logger.debug(f"{self.log_prefix}: New aircraft detected: {icao}. Checking for historical track.")
                        # ...chiediamo al controller la sua traccia storica per oggi
                        historical_track_data = self.app_controller.get_historical_track_for_icao(icao)
                        
                        # Creiamo un nuovo deque per la traccia
                        self.flight_tracks_gui[icao] = deque(maxlen=self.max_track_points + 5)
                        
                        if historical_track_data:
                            logger.info(f"{self.log_prefix}: Found {len(historical_track_data)} historical points for {icao}. Pre-populating track.")
                            # Popoliamo il deque con i punti storici
                            for point in historical_track_data:
                                lat, lon, ts = point.get("latitude"), point.get("longitude"), point.get("timestamp")
                                if lat is not None and lon is not None and ts is not None:
                                    self.flight_tracks_gui[icao].append((lat, lon, ts))
                    
                    # --- FINE NUOVA LOGICA ---

                    # Aggiungiamo il nuovo punto live alla traccia (se il deque esiste)
                    if state.latitude is not None and state.longitude is not None and state.timestamp is not None:
                        if icao in self.flight_tracks_gui:
                            self.flight_tracks_gui[icao].append((state.latitude, state.longitude, state.timestamp))
                        else:
                            # Questo caso non dovrebbe accadere con la nuova logica, ma per sicurezza...
                            self.flight_tracks_gui[icao] = deque(maxlen=self.max_track_points + 5)
                            self.flight_tracks_gui[icao].append((state.latitude, state.longitude, state.timestamp))
                
                # Pulizia delle tracce per aerei non più visibili (logica esistente)
                tracks_to_remove_gui = []
                for icao, track_deque in self.flight_tracks_gui.items():
                    if not track_deque:
                        tracks_to_remove_gui.append(icao)
                        continue
                    if icao not in active_icao_this_update:
                        last_point_time = track_deque[-1][2]
                        if current_time - last_point_time > self.max_track_age_seconds:
                            tracks_to_remove_gui.append(icao)
                
                if tracks_to_remove_gui:
                    logger.debug(f"{self.log_prefix}: Removing {len(tracks_to_remove_gui)} old tracks.")
                    for icao in tracks_to_remove_gui:
                        if icao in self.flight_tracks_gui:
                            del self.flight_tracks_gui[icao]
        
        # Richiedi un re-render della mappa con i dati aggiornati
        if (self._current_center_lat_gui is not None and
            self._current_center_lon_gui is not None and
            self._current_zoom_gui is not None):
            logger.info(f"{self.log_prefix} update_flights_on_map: Requesting map re-render due to flight update.")
            self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui)

    def get_current_map_info(self) -> Dict[str, Any]:
        logger.debug(f"{self.log_prefix} get_current_map_info (GUI) called.")
        map_size_km_w, map_size_km_h = None, None
        if (PYPROJ_MODULE_LOCALLY_AVAILABLE and map_utils.pyproj and self._current_map_geo_bounds_gui):
            try:
                size_km_tuple = calculate_geographic_bbox_size_km(self._current_map_geo_bounds_gui)
                if size_km_tuple: map_size_km_w, map_size_km_h = size_km_tuple
            except Exception as e_map_size_info:
                logger.warning(f"Error calculating current map geo size for info panel ({self.log_prefix}): {e_map_size_info}", exc_info=False)

        with self._map_data_lock:
            flight_count = len(self._current_flights_to_display_gui)
            target_bbox_copy = copy.deepcopy(self._target_bbox_input_gui) if self._target_bbox_input_gui else None

        info = {
            "center_lat": self._current_center_lat_gui, "center_lon": self._current_center_lon_gui,
            "zoom": self._current_zoom_gui, "map_geo_bounds": self._current_map_geo_bounds_gui,
            "target_bbox_input": target_bbox_copy, "canvas_width": self.canvas_width,
            "canvas_height": self.canvas_height, "map_size_km_w": map_size_km_w,
            "map_size_km_h": map_size_km_h, "flight_count": flight_count,
        }
        logger.debug(f"{self.log_prefix} get_current_map_info returning: Flights={info['flight_count']}, Zoom={info['zoom']}, Center=({info['center_lat']},{info['center_lon']})")
        return info

    def show_map_context_menu_from_gui(
        self, latitude: float, longitude: float, screen_x: int, screen_y: int
    ):
        if self.is_detail_map:
            logger.warning("show_map_context_menu_from_gui called on a detail map. Ignoring.")
            return
        logger.info(f"MCM show_map_context_menu_from_gui: Lat {latitude:.4f}, Lon {longitude:.4f}")
        if not self.canvas.winfo_exists():
            logger.warning("MCM show_map_context_menu_from_gui: Canvas does not exist.")
            return
        root_widget = self.canvas.winfo_toplevel()
        try:
            context_menu = tk.Menu(root_widget, tearoff=0)
            decimals = getattr(map_constants, "COORDINATE_DECIMAL_PLACES", 5)
            context_menu.add_command(label=f"Context @ {latitude:.{decimals}f},{longitude:.{decimals}f}", state=tk.DISABLED)
            context_menu.add_separator()
            if self.app_controller:
                if hasattr(self.app_controller, "recenter_map_at_coords"):
                    context_menu.add_command(label="Center map here", command=lambda: self.app_controller.recenter_map_at_coords(latitude, longitude))
                if hasattr(self.app_controller, "set_bbox_around_coords"):
                    area_km_cfg_name = "DEFAULT_CLICK_AREA_SIZE_KM"
                    area_km = getattr(self.app_controller, area_km_cfg_name, 50.0)
                    context_menu.add_command(label=f"Set {area_km:.0f}km Mon. Area", command=lambda: self.app_controller.set_bbox_around_coords(latitude, longitude, area_km))
            context_menu.tk_popup(screen_x, screen_y)
        except tk.TclError as e: logger.warning(f"TclError showing MapCanvasManager context menu: {e}.")
        except Exception as e: logger.error(f"Error creating/showing MapCanvasManager context menu: {e}", exc_info=True)

    def recenter_map_at_coords(self, lat: float, lon: float):
        logger.info(f"{self.log_prefix} recenter_map_at_coords (GUI): Request to recenter map @ Geo ({lat:.4f}, {lon:.4f})")
        if self._current_zoom_gui is not None and self.canvas.winfo_exists():
            self._current_center_lat_gui = lat
            self._current_center_lon_gui = lon
            self._request_map_render(lat, lon, self._current_zoom_gui)
        else:
            logger.warning(f"{self.log_prefix} recenter_map_at_coords: Cannot recenter map - missing current zoom or canvas not available.")

    def set_bbox_around_coords(self, center_lat: float, center_lon: float, area_size_km: float):
        if self.is_detail_map:
            logger.warning(f"{self.log_prefix} set_bbox_around_coords called on a DETAIL map. This action is for the main map.")
            return

        logger.info(f"{self.log_prefix} set_bbox_around_coords (MAIN MAP): Request to set BBox around Geo ({center_lat:.4f}, {center_lon:.4f}), size {area_size_km:.1f}km.")
        if not PYPROJ_MODULE_LOCALLY_AVAILABLE or map_utils.pyproj is None:
            logger.error(f"{self.log_prefix} set_bbox_around_coords: Cannot set BBox - pyproj library not available.")
            if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"):
                self.app_controller.main_window.show_error_message("Map Error", "Cannot calculate BBox: Geographic libraries missing.")
            return
        try:
            bbox_tuple_wesn = get_bounding_box_from_center_size(center_lat, center_lon, area_size_km)
            if bbox_tuple_wesn:
                bbox_dict = {"lon_min": bbox_tuple_wesn[0], "lat_min": bbox_tuple_wesn[1], "lon_max": bbox_tuple_wesn[2], "lat_max": bbox_tuple_wesn[3]}
                if _is_valid_bbox_dict(bbox_dict):
                    self.set_target_bbox(bbox_dict)
                else:
                    logger.error(f"{self.log_prefix} set_bbox_around_coords: Calculated BBox is invalid: {bbox_dict}.")
                    if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"):
                        self.app_controller.main_window.show_error_message("Map Error", "Calculated BBox is invalid.")
            else:
                logger.error(f"{self.log_prefix} set_bbox_around_coords: Failed to calculate BBox around coords.")
                if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"):
                    self.app_controller.main_window.show_error_message("Map Error", "Failed to calculate BBox around coordinates.")
        except Exception as e:
            logger.exception(f"{self.log_prefix} set_bbox_around_coords: Unexpected error calculating BBox: {e}")
            if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"):
                self.app_controller.main_window.show_error_message("Map Error", f"An unexpected error occurred calculating BBox: {e}")

    def zoom_in_at_center(self):
        logger.debug(f"{self.log_prefix} zoom_in_at_center (GUI) called. Current zoom: {self._current_zoom_gui}")
        if (self._current_zoom_gui is None or self._current_center_lat_gui is None or self._current_center_lon_gui is None):
            logger.warning(f"{self.log_prefix} zoom_in_at_center: Cannot zoom in - current map state is not defined.")
            return
        if not self.canvas.winfo_exists(): return
        max_zoom_svc = self.map_service.max_zoom if self.map_service else map_constants.DEFAULT_MAX_ZOOM_FALLBACK
        new_zoom = min(self._current_zoom_gui + 1, max_zoom_svc)
        if new_zoom != self._current_zoom_gui:
            self._current_zoom_gui = new_zoom
            self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, new_zoom)

    def zoom_out_at_center(self):
        logger.debug(f"{self.log_prefix} zoom_out_at_center (GUI) called. Current zoom: {self._current_zoom_gui}")
        if (self._current_zoom_gui is None or self._current_center_lat_gui is None or self._current_center_lon_gui is None):
            logger.warning(f"{self.log_prefix} zoom_out_at_center: Cannot zoom out - current map state is not defined.")
            return
        if not self.canvas.winfo_exists(): return
        new_zoom = max(map_constants.MIN_ZOOM_LEVEL, self._current_zoom_gui - 1)
        if new_zoom != self._current_zoom_gui:
            self._current_zoom_gui = new_zoom
            self._request_map_render(self._current_center_lat_gui, self._current_center_lon_gui, new_zoom)

    def pan_map_fixed_step(self, direction: str, step_fraction: float = PAN_STEP_FRACTION):
        logger.debug(f"{self.log_prefix} pan_map_fixed_step (GUI) called. Direction: {direction}")
        if (self._current_center_lat_gui is None or self._current_center_lon_gui is None or self._current_zoom_gui is None):
            logger.warning(f"{self.log_prefix} pan_map_fixed_step: Cannot pan map - current map state not fully defined.")
            return
        if (not self.canvas.winfo_exists() or not map_utils.PYPROJ_MODULE_LOCALLY_AVAILABLE or map_utils.pyproj is None): # MODIFIED: Use map_utils.PYPROJ_MODULE_LOCALLY_AVAILABLE
            logger.warning(f"{self.log_prefix} pan_map_fixed_step: Cannot pan map - canvas or PyProj library not available.")
            return

        delta_x_pixels, delta_y_pixels = 0, 0
        pan_step_px_w = int(self.canvas_width * step_fraction)
        pan_step_px_h = int(self.canvas_height * step_fraction)

        if direction == "left": delta_x_pixels = -pan_step_px_w
        elif direction == "right": delta_x_pixels = pan_step_px_w
        elif direction == "up": delta_y_pixels = -pan_step_px_h
        elif direction == "down": delta_y_pixels = pan_step_px_h
        else: logger.warning(f"{self.log_prefix} pan_map_fixed_step: Unknown pan direction: {direction}"); return

        res_m_px = calculate_meters_per_pixel(self._current_center_lat_gui, self._current_zoom_gui, self.tile_manager.tile_size)
        if res_m_px is None or res_m_px <= 1e-9:
            logger.error(f"{self.log_prefix} pan_map_fixed_step: Could not calculate valid resolution for panning. Cannot pan.")
            return

        delta_meters_x = delta_x_pixels * res_m_px
        delta_meters_y = -delta_y_pixels * res_m_px
        geod = map_utils.pyproj.Geod(ellps="WGS84")
        current_calc_lon, current_calc_lat = self._current_center_lon_gui, self._current_center_lat_gui

        if abs(delta_meters_x) > 1e-9:
            azimuth_lon = 90.0 if delta_meters_x > 0 else 270.0
            new_lon, _, _ = geod.fwd(current_calc_lon, current_calc_lat, azimuth_lon, abs(delta_meters_x))
            current_calc_lon = new_lon
        if abs(delta_meters_y) > 1e-9:
            azimuth_lat = 0.0 if delta_meters_y > 0 else 180.0
            _, new_lat, _ = geod.fwd(current_calc_lon, current_calc_lat, azimuth_lat, abs(delta_meters_y))
            current_calc_lat = new_lat

        MAX_MERCATOR_LAT = 85.05112878
        final_new_center_lat = max(-MAX_MERCATOR_LAT, min(MAX_MERCATOR_LAT, current_calc_lat))
        final_new_center_lon = (current_calc_lon + 180) % 360 - 180
        if final_new_center_lon == -180 and current_calc_lon > 0: final_new_center_lon = 180.0

        self._current_center_lat_gui = final_new_center_lat
        self._current_center_lon_gui = final_new_center_lon
        self._request_map_render(final_new_center_lat, final_new_center_lon, self._current_zoom_gui)

    def center_map_and_fit_patch(self, center_lat: float, center_lon: float, patch_size_km: float):
        logger.debug(f"{self.log_prefix} Center map at ({center_lat:.4f},{center_lon:.4f}) and fit {patch_size_km}km patch.")
        if (not self.canvas.winfo_exists() or self.canvas_width <= 0 or self.canvas_height <= 0):
            logger.error(f"{self.log_prefix} Center map and fit patch: Cannot fit patch - canvas not ready or invalid dimensions.")
            if self.app_controller and hasattr(self.app_controller, "main_window") and self.app_controller.main_window and hasattr(self.app_controller.main_window, "show_error_message"):
                self.app_controller.main_window.show_error_message("Map Error", "Canvas not ready to fit patch.")
            return

        zoom_for_width = calculate_zoom_level_for_geographic_size(center_lat, patch_size_km * 1000, self.canvas_width, self.tile_manager.tile_size)
        zoom_for_height = calculate_zoom_level_for_geographic_size(center_lat, patch_size_km * 1000, self.canvas_height, self.tile_manager.tile_size)

        if zoom_for_width is None or zoom_for_height is None:
            logger.error(f"{self.log_prefix} Center map and fit patch: Could not calculate zoom to fit patch. Using current or default zoom.")
            new_zoom = self._current_zoom_gui if self._current_zoom_gui is not None else map_constants.DEFAULT_INITIAL_ZOOM
        else: new_zoom = min(zoom_for_width, zoom_for_height)

        max_zoom_limit_svc = self.map_service.max_zoom if self.map_service else map_constants.DEFAULT_MAX_ZOOM_FALLBACK
        new_zoom = max(map_constants.MIN_ZOOM_LEVEL, min(new_zoom, max_zoom_limit_svc))
        
        if new_zoom > map_constants.MIN_ZOOM_LEVEL:
            new_zoom -=1
            logger.info(f"{self.log_prefix} Center map and fit patch: Applied -1 zoom margin for patch. New zoom: {new_zoom}")


        self._current_center_lat_gui = center_lat
        self._current_center_lon_gui = center_lon
        self._current_zoom_gui = new_zoom
        self._request_map_render(center_lat, center_lon, new_zoom)

    def shutdown_worker(self):
        logger.info(f"{self.log_prefix} Shutdown worker requested.")
        # MODIFIED: Delegate worker shutdown to MapRenderManager
        # WHY: MapRenderManager is responsible for the worker's lifecycle.
        # HOW: Call self.map_render_manager.shutdown_worker().
        self.map_render_manager.shutdown_worker()

        # MODIFIED: Removed old worker thread cleanup (now handled by MapRenderManager)
        # if self._gui_after_id_result_processor and self.canvas.winfo_exists():
        #     try: self.canvas.after_cancel(self._gui_after_id_result_processor)
        #     except Exception: pass
        # self._gui_after_id_result_processor = None
        # try: self._map_render_request_queue.put_nowait({"type": RENDER_REQUEST_TYPE_SHUTDOWN, "request_id": -999})
        # except queue.Full: logger.warning(f"{self.log_prefix} Worker request queue full, cannot send shutdown signal via queue.")
        # except Exception as e: logger.error(f"{self.log_prefix} Error sending shutdown signal to worker queue: {e}")
        # if self._map_worker_thread and self._map_worker_thread.is_alive():
        #     logger.info(f"{self.log_prefix} Waiting for map worker thread to join...")
        #     self._map_worker_thread.join(timeout=2.0)
        #     if self._map_worker_thread.is_alive(): logger.warning(f"{self.log_prefix} Map worker thread did not join in time.")
        #     else: logger.info(f"{self.log_prefix} Map worker thread joined successfully.")
        # self._map_worker_thread = None
        # while not self._map_render_request_queue.empty():
        #     try: self._map_render_request_queue.get_nowait(); self._map_render_request_queue.task_done()
        #     except Exception: break
        # while not self._map_render_result_queue.empty():
        #     try: self._map_render_result_queue.get_nowait(); self._map_render_result_queue.task_done()
        #     except Exception: break
        # module_logger.info(f"{self.log_prefix} Worker shutdown sequence complete.")