# 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 threading
import copy

try:
    from PIL import Image, ImageTk, ImageDraw, ImageFont
    PIL_IMAGE_LIB_AVAILABLE = True
    ImageType = Image.Image
    PhotoImageType = ImageTk.PhotoImage
except ImportError:
    ImageType = Any
    PhotoImageType = Any
    PIL_IMAGE_LIB_AVAILABLE = False
    import logging
    logging.error("MapCanvasManager: Pillow not found. Map disabled.")

from flightmonitor.map.map_utils import (
    PYPROJ_MODULE_LOCALLY_AVAILABLE,
    MERCANTILE_MODULE_LOCALLY_AVAILABLE,
    geo_to_pixel_on_unscaled_map,
    _is_valid_bbox_dict,
    calculate_geographic_bbox_size_km,
    calculate_zoom_level_for_geographic_size,
    get_bounding_box_from_center_size,
    pixel_to_geo,
    calculate_meters_per_pixel,
    calculate_geographic_bbox_from_pixel_size_and_zoom,
    get_tile_ranges_for_bbox,
)
from flightmonitor.map import map_constants
from flightmonitor.data import config as app_config
from flightmonitor.data.common_models import CanonicalFlightState
from flightmonitor.map.map_services import BaseMapService, OpenStreetMapService
from flightmonitor.map.map_tile_manager import MapTileManager
from flightmonitor.map.map_render_manager import (
    MapRenderManager,
    RENDER_REQUEST_BASE_MAP,
    RENDER_REQUEST_OVERLAY,
)
from flightmonitor.map import map_drawing

try:
    from flightmonitor.utils.logger import get_logger
    logger = get_logger(__name__)
except ImportError:
    import logging
    logger = logging.getLogger(__name__)
    if not logger.hasHandlers():
        logging.basicConfig(level=logging.INFO)
    logger.warning("MapCanvasManager using fallback standard Python logger.")
    
try:
    import pyproj
except ImportError:
    pyproj = None
    
try:
    import mercantile
    MERCANTILE_MODULE_LOCALLY_AVAILABLE = True
except ImportError as e_mercantile:
    mercantile = None
    # MODIFIED: Ensure logging is imported for fallback messages.
    # WHY: Log error even if mercantile fails.
    # HOW: Added import.
    import logging
    logging.error(f"MapCanvasManager: 'mercantile' not found: {e_mercantile}. Tile and Mercator conversions impaired.")

CANVAS_SIZE_HARD_FALLBACK_PX = 800
RESIZE_DEBOUNCE_DELAY_MS = 250
PAN_STEP_FRACTION = 0.25
GUI_RESULT_POLL_INTERVAL_MS = 50
CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT = 0.10
AIRCRAFT_VISIBILITY_TIMEOUT_SECONDS = 300


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})"

        if not (PIL_IMAGE_LIB_AVAILABLE and MERCANTILE_MODULE_LOCALLY_AVAILABLE):
            raise ImportError(f"{self.log_prefix}: Critical dependencies missing.")

        self.app_controller = app_controller
        self.canvas = tk_canvas
        self.canvas_width = tk_canvas.winfo_width() or CANVAS_SIZE_HARD_FALLBACK_PX
        self.canvas_height = tk_canvas.winfo_height() or app_config.DEFAULT_CANVAS_HEIGHT

        # --- State Variables ---
        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

        # --- Layer Management ---
        self._base_map_photo_image: Optional[PhotoImageType] = None
        self._canvas_base_map_id: Optional[int] = None
        self._overlay_photo_image: Optional[PhotoImageType] = None
        self._canvas_overlay_id: Optional[int] = None

        self._placeholder_text_id: Optional[int] = None

        # --- Components ---
        self.map_service: BaseMapService = OpenStreetMapService()
        self.tile_manager: MapTileManager = MapTileManager(map_service=self.map_service)
        self.map_render_manager = MapRenderManager()

        # --- Data for Drawing ---
        self._current_flights_to_display_gui: List[CanonicalFlightState] = []
        self._active_aircraft_states: Dict[str, CanonicalFlightState] = {}
        self.flight_tracks_gui: Dict[str, deque] = {}
        self.max_track_points: int = app_config.DEFAULT_TRACK_HISTORY_POINTS
        self._map_data_lock: threading.Lock = threading.Lock()

        # --- Event Handling ---
        self._resize_debounce_job_id: Optional[str] = None
        self._gui_after_id_result_processor: Optional[str] = None
        self._drag_start_x_canvas, self._drag_start_y_canvas = None, None

        self.map_render_manager.set_render_pipeline_callable(self._execute_render_pipeline)
        self.map_render_manager.start_worker()

        if initial_bbox_dict and _is_valid_bbox_dict(initial_bbox_dict):
            self.set_target_bbox(initial_bbox_dict)
        else:
            self._display_placeholder_text("Ready")

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

    def _execute_render_pipeline(self, params: Dict[str, Any], render_base: bool, render_overlay: bool):
        base_map_image: Optional[ImageType] = None
        overlay_image: Optional[ImageType] = None
        final_bounds: Optional[Tuple[float, float, float, float]] = None

        canvas_w = params.get("canvas_width")
        canvas_h = params.get("canvas_height")

        if render_base:
            center_lat = params.get("center_lat")
            center_lon = params.get("center_lon")
            zoom = params.get("zoom")
            
            # Render a larger area to handle panning smoothly at the edges
            render_w = int(canvas_w * 1.5)
            render_h = int(canvas_h * 1.5)

            oversized_bbox = calculate_geographic_bbox_from_pixel_size_and_zoom(
                center_lat, center_lon, render_w, render_h, zoom, self.tile_manager.tile_size
            )
            if not oversized_bbox:
                return None, None, None, "Failed to calculate oversized bbox for rendering"

            tile_ranges = get_tile_ranges_for_bbox(oversized_bbox, zoom)
            if not tile_ranges:
                return None, None, None, "Failed to get tile ranges for bbox"

            stitched_map = self.tile_manager.stitch_map_image(zoom, tile_ranges[0], tile_ranges[1])
            if not stitched_map:
                return None, None, None, "Failed to stitch map"

            stitched_bounds = self.tile_manager._get_bounds_for_tile_range(zoom, tile_ranges)
            if not stitched_bounds:
                return None, None, None, "Failed to get bounds for stitched map"

            center_px = geo_to_pixel_on_unscaled_map(center_lat, center_lon, stitched_bounds, stitched_map.size)
            if not center_px:
                center_px = (stitched_map.width / 2, stitched_map.height / 2)

            crop_x0, crop_y0 = round(center_px[0] - canvas_w / 2), round(center_px[1] - canvas_h / 2)
            base_map_image = stitched_map.crop((crop_x0, crop_y0, crop_x0 + canvas_w, crop_y0 + canvas_h))
            
            # --- MODIFICA CRITICA PER ALLINEAMENTO ---
            # Calcoliamo i nuovi "bounds" in base al crop, non li ricalcoliamo da zero.
            # Questo garantisce che i bounds corrispondano esattamente all'immagine ritagliata.
            if MERCANTILE_MODULE_LOCALLY_AVAILABLE:
                stitched_ul_merc_x, stitched_ul_merc_y = mercantile.xy(stitched_bounds[0], stitched_bounds[3])
                stitched_lr_merc_x, stitched_lr_merc_y = mercantile.xy(stitched_bounds[2], stitched_bounds[1])
                
                merc_per_px_x = (stitched_lr_merc_x - stitched_ul_merc_x) / stitched_map.width
                merc_per_px_y = (stitched_lr_merc_y - stitched_ul_merc_y) / stitched_map.height

                final_ul_merc_x = stitched_ul_merc_x + crop_x0 * merc_per_px_x
                final_ul_merc_y = stitched_ul_merc_y + crop_y0 * merc_per_px_y
                final_lr_merc_x = final_ul_merc_x + canvas_w * merc_per_px_x
                final_lr_merc_y = final_ul_merc_y + canvas_h * merc_per_px_y
                
                final_west, final_north = mercantile.lnglat(final_ul_merc_x, final_ul_merc_y)
                final_east, final_south = mercantile.lnglat(final_lr_merc_x, final_lr_merc_y)
                
                final_bounds = (final_west, final_south, final_east, final_north)
            else:
                 return None, None, None, "Mercantile not available for final bounds calculation"

            if not final_bounds:
                return None, None, None, "Failed to calculate final map bounds from crop"
            
            # Disegna elementi statici sulla mappa di base
            draw_base = ImageDraw.Draw(base_map_image)
            if map_constants.GRATICULE_ENABLED:
                graticule_font = map_drawing._load_label_font(0, is_graticule=True)
                map_drawing.draw_graticules(draw_base, final_bounds, base_map_image.size, graticule_font)

            target_bbox = params.get("target_bbox")
            draw_target_bbox = params.get("draw_target_bbox_overlay", False)
            if draw_target_bbox and target_bbox and _is_valid_bbox_dict(target_bbox):
                bbox_wesn = (target_bbox["lon_min"], target_bbox["lat_min"], target_bbox["lon_max"], target_bbox["lat_max"])
                map_drawing.draw_area_bounding_box(draw_base, bbox_wesn, final_bounds, base_map_image.size, "blue", 2)
        
        if render_overlay:
            bounds_for_overlay = final_bounds if render_base else params.get("map_geo_bounds")
            if not bounds_for_overlay:
                return None, None, None, "Missing map bounds for overlay rendering"

            overlay_image = Image.new("RGBA", (canvas_w, canvas_h), (0, 0, 0, 0))
            draw_overlay = ImageDraw.Draw(overlay_image)
            
            flights = params.get("flights", [])
            tracks = params.get("tracks", {})
            zoom = params.get("zoom")
            
            if flights:
                label_font_size = 10 + (zoom - 10)
                flight_label_font = map_drawing._load_label_font(label_font_size)
                for flight in flights:
                    if flight.latitude is not None and flight.longitude is not None:
                        pixel_coords = geo_to_pixel_on_unscaled_map(
                            flight.latitude, flight.longitude, bounds_for_overlay, overlay_image.size
                        )
                        if pixel_coords:
                            map_drawing._draw_single_flight(
                                draw_overlay, pixel_coords, flight, flight_label_font,
                                tracks.get(flight.icao24), bounds_for_overlay, overlay_image.size
                            )
                            
        return base_map_image, overlay_image, final_bounds, None

    def _start_gui_result_processing(self):
        if self._gui_after_id_result_processor and self.canvas.winfo_exists():
            self.canvas.after_cancel(self._gui_after_id_result_processor)
        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 _process_map_render_results(self):
        if not self.canvas.winfo_exists():
            self._gui_after_id_result_processor = None
            return
        try:
            while True:
                result = self.map_render_manager.get_render_result()
                if not result: break

                req_id = result.get("request_id")
                if req_id < self.map_render_manager.get_expected_gui_render_id():
                    continue

                if result.get("error"):
                    self._display_placeholder_text(f"Map Error:\n{result['error'][:100]}")
                    continue

                req_type = result.get("type")
                if req_type == RENDER_REQUEST_BASE_MAP:
                    self._clear_canvas_display_elements()
                    self._base_map_photo_image = result.get("base_map_photo")
                    self._overlay_photo_image = result.get("overlay_photo")
                    self._current_map_geo_bounds_gui = result.get("map_geo_bounds")

                    if self._base_map_photo_image:
                        self._canvas_base_map_id = self.canvas.create_image(0, 0, anchor=tk.NW, image=self._base_map_photo_image)
                    if self._overlay_photo_image:
                        self._canvas_overlay_id = self.canvas.create_image(0, 0, anchor=tk.NW, image=self._overlay_photo_image)

                elif req_type == RENDER_REQUEST_OVERLAY:
                    if self._canvas_overlay_id:
                        self.canvas.delete(self._canvas_overlay_id)
                    self._overlay_photo_image = result.get("overlay_photo")
                    if self._overlay_photo_image:
                        self._canvas_overlay_id = self.canvas.create_image(0, 0, anchor=tk.NW, image=self._overlay_photo_image)

                if not self.is_detail_map:
                    self.app_controller.update_general_map_info()

        except Exception as e:
            logger.exception(f"Error processing map 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_base_map_render(self, center_lat: float, center_lon: float, zoom_level: int):
        if not self.map_render_manager.is_worker_alive():
            self._display_placeholder_text("Map Worker Offline")
            return

        with self._map_data_lock:
            params = {
                "center_lat": center_lat, "center_lon": center_lon, "zoom": zoom_level,
                "canvas_width": self.canvas_width, "canvas_height": self.canvas_height,
                "target_bbox": copy.deepcopy(self._target_bbox_input_gui),
                "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),
            }
        
        req_id = self.map_render_manager.put_render_request(RENDER_REQUEST_BASE_MAP, params)
        if req_id is not None:
            self.map_render_manager.set_expected_gui_render_id(req_id)
            self._display_placeholder_text(f"Loading Map... Z{zoom_level}")
            self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui = center_lat, center_lon, zoom_level

    def _request_overlay_render(self):
        if not self.map_render_manager.is_worker_alive() or not self._current_map_geo_bounds_gui:
            return

        with self._map_data_lock:
            params = {
                "canvas_width": self.canvas_width, "canvas_height": self.canvas_height,
                "map_geo_bounds": self._current_map_geo_bounds_gui,
                "zoom": self._current_zoom_gui,
                "flights": copy.deepcopy(self._current_flights_to_display_gui),
                "tracks": copy.deepcopy(self.flight_tracks_gui),
            }
        
        req_id = self.map_render_manager.put_render_request(RENDER_REQUEST_OVERLAY, params)
        if req_id is not None:
            self.map_render_manager.set_expected_gui_render_id(req_id)

    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("<B1-Motion>", self._on_mouse_drag)
        self.canvas.bind("<ButtonRelease-1>", self._on_left_button_release)
        self.canvas.bind("<ButtonPress-3>", self._on_right_click)
        self.canvas.bind("<MouseWheel>", self._on_mouse_wheel)
        self.canvas.bind("<Button-4>", self._on_mouse_wheel_linux)
        self.canvas.bind("<Button-5>", self._on_mouse_wheel_linux)

    def _on_canvas_resize(self, event: tk.Event):
        if event.width > 1 and event.height > 1 and (self.canvas_width != event.width or self.canvas_height != event.height):
            if self._resize_debounce_job_id:
                self.canvas.after_cancel(self._resize_debounce_job_id)
            self._resize_debounce_job_id = self.canvas.after(RESIZE_DEBOUNCE_DELAY_MS, self._perform_resize_redraw, event.width, event.height)

    def _perform_resize_redraw(self, width: int, height: int):
        self._resize_debounce_job_id = None
        if not self.canvas.winfo_exists(): return
        self.canvas_width, self.canvas_height = width, height
        if self._current_center_lat_gui is not None:
            self._request_base_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui)

    def _on_left_button_press(self, event: tk.Event):
        self._drag_start_x_canvas, self._drag_start_y_canvas = event.x, event.y

    def _on_mouse_drag(self, event: tk.Event):
        if self._drag_start_x_canvas is None: return
        dx, dy = event.x - self._drag_start_x_canvas, event.y - self._drag_start_y_canvas
        if abs(dx) < 5 and abs(dy) < 5: return
        
        self._drag_start_x_canvas, self._drag_start_y_canvas = event.x, event.y
        if not all([self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui, PYPROJ_MODULE_LOCALLY_AVAILABLE]):
            return
            
        res = calculate_meters_per_pixel(self._current_center_lat_gui, self._current_zoom_gui, self.tile_manager.tile_size)
        if not res: return
        
        dmx, dmy = -dx * res, dy * res
        geod = pyproj.Geod(ellps="WGS84")
        new_lon, new_lat, _ = geod.fwd(self._current_center_lon_gui, self._current_center_lat_gui, 90, dmx)
        new_lon, new_lat, _ = geod.fwd(new_lon, new_lat, 0, dmy)
        self._request_base_map_render(new_lat, new_lon, self._current_zoom_gui)

    def _on_left_button_release(self, event: tk.Event):
        if self._drag_start_x_canvas is not None and abs(event.x - self._drag_start_x_canvas) < 5 and abs(event.y - self._drag_start_y_canvas) < 5:
            if self._current_map_geo_bounds_gui:
                map_size = (self.canvas_width, self.canvas_height)
                lon, lat = pixel_to_geo(event.x, event.y, self._current_map_geo_bounds_gui, map_size)
                if lon is not None and lat is not None:
                    self.app_controller.on_map_left_click(lat, lon, event.x, event.y, event.x_root, event.y_root)
        self._drag_start_x_canvas = None

    def _on_mouse_wheel(self, event: tk.Event):
        if self._current_zoom_gui is None: return
        self.zoom_in_at_center() if event.delta > 0 else self.zoom_out_at_center()

    def _on_mouse_wheel_linux(self, event: tk.Event):
        if self._current_zoom_gui is None: return
        self.zoom_in_at_center() if event.num == 4 else self.zoom_out_at_center()

    def get_clicked_flight_icao(self, canvas_x: int, canvas_y: int) -> Optional[str]:
        if not self._current_map_geo_bounds_gui: return None
        min_dist_sq, clicked_icao = 15**2, None
        map_size = (self.canvas_width, self.canvas_height)
        
        with self._map_data_lock:
            flights_to_check = list(self._active_aircraft_states.values()) if self._active_aircraft_states else self._current_flights_to_display_gui

        for flight in flights_to_check:
            if flight.latitude is not None and flight.longitude is not None:
                px = geo_to_pixel_on_unscaled_map(flight.latitude, flight.longitude, self._current_map_geo_bounds_gui, map_size)
                if px:
                    dist_sq = (canvas_x - px[0]) ** 2 + (canvas_y - px[1]) ** 2
                    if dist_sq < min_dist_sq:
                        min_dist_sq, clicked_icao = dist_sq, flight.icao24
        return clicked_icao

    def _on_right_click(self, event: tk.Event):
        if self._current_map_geo_bounds_gui:
            map_size = (self.canvas_width, self.canvas_height)
            lon, lat = pixel_to_geo(event.x, event.y, self._current_map_geo_bounds_gui, map_size)
            if lon is not None and lat is not None:
                self.app_controller.on_map_right_click(lat, lon, event.x_root, event.y_root)

    def set_max_track_points(self, length: int):
        self.max_track_points = max(2, length)
        # Existing deques will be implicitly resized on next append
        self._request_overlay_render()

    def set_target_bbox(self, bbox_dict: Optional[Dict[str, float]]):
        if bbox_dict and _is_valid_bbox_dict(bbox_dict):
            with self._map_data_lock:
                self._target_bbox_input_gui = bbox_dict.copy()
            self._request_map_render_for_bbox(bbox_dict)
            if not self.is_detail_map:
                self.app_controller.update_bbox_gui_fields(bbox_dict)
        else:
            with self._map_data_lock:
                self._target_bbox_input_gui = None
            if self._current_center_lat_gui:
                self._request_base_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui)
            if not self.is_detail_map:
                self.app_controller.update_bbox_gui_fields({})

    def _request_map_render_for_bbox(self, bbox: Dict[str, float], preserve_zoom: bool = False):
        center_lat, center_lon = (bbox["lat_min"] + bbox["lat_max"]) / 2, (bbox["lon_min"] + bbox["lon_max"]) / 2
        zoom = self._current_zoom_gui
        if not preserve_zoom or not zoom:
            size_km = calculate_geographic_bbox_size_km((bbox["lon_min"], bbox["lat_min"], bbox["lon_max"], bbox["lat_max"]))
            if size_km:
                w, h = self.canvas_width * (1 - CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT), self.canvas_height * (1 - CANVAS_MARGIN_FACTOR_FOR_BBOX_FIT)
                zoom_w = calculate_zoom_level_for_geographic_size(center_lat, size_km[0] * 1000, int(w), self.tile_manager.tile_size)
                zoom_h = calculate_zoom_level_for_geographic_size(center_lat, size_km[1] * 1000, int(h), self.tile_manager.tile_size)
                zoom = min(zoom_w, zoom_h) if zoom_w and zoom_h else map_constants.DEFAULT_INITIAL_ZOOM
        self._request_base_map_render(center_lat, center_lon, zoom)

    def _clear_canvas_display_elements(self):
        if self.canvas.winfo_exists():
            if self._canvas_base_map_id: self.canvas.delete(self._canvas_base_map_id)
            if self._canvas_overlay_id: self.canvas.delete(self._canvas_overlay_id)
            if self._placeholder_text_id: self.canvas.delete(self._placeholder_text_id)
        self._canvas_base_map_id, self._canvas_overlay_id, self._placeholder_text_id = None, None, None
        self._base_map_photo_image, self._overlay_photo_image = None, None
        
    def _display_placeholder_text(self, text: str):
        if not self.canvas.winfo_exists(): return
        self._clear_canvas_display_elements()
        self.canvas.configure(bg=map_constants.DEFAULT_PLACEHOLDER_COLOR_RGB_TK)
        w, h = self.canvas_width, self.canvas_height
        if w > 1 and h > 1:
            self._placeholder_text_id = self.canvas.create_text(w / 2, h / 2, text=text, fill="gray10", font=("Arial", 11))

    def clear_map_display(self):
        self._display_placeholder_text("Map Cleared")
        with self._map_data_lock:
            self._current_flights_to_display_gui.clear()
            self._active_aircraft_states.clear()
            self.flight_tracks_gui.clear()
        if self._current_center_lat_gui:
            self._request_base_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui)

    def update_flights_on_map(self, flight_states: List[CanonicalFlightState]):
        with self._map_data_lock:
            self._current_flights_to_display_gui = flight_states
            active_icao = {s.icao24 for s in flight_states}
            for icao in list(self.flight_tracks_gui.keys()):
                if icao not in active_icao:
                    del self.flight_tracks_gui[icao]
            for state in flight_states:
                if state.icao24 not in self.flight_tracks_gui:
                    self.flight_tracks_gui[state.icao24] = deque(maxlen=self.max_track_points + 5)
                if state.latitude is not None and state.longitude is not None and state.timestamp is not None:
                    self.flight_tracks_gui[state.icao24].append((state.latitude, state.longitude, state.timestamp))
        self._request_overlay_render()

    def update_playback_frame(self, new_flight_states: List[CanonicalFlightState], virtual_timestamp: float):
        with self._map_data_lock:
            timed_out_icaos = {icao for icao, state in self._active_aircraft_states.items() if virtual_timestamp - state.last_contact_timestamp > AIRCRAFT_VISIBILITY_TIMEOUT_SECONDS}
            for icao in timed_out_icaos:
                del self._active_aircraft_states[icao]
                if icao in self.flight_tracks_gui: del self.flight_tracks_gui[icao]
            
            for state in new_flight_states:
                state.last_contact_timestamp = virtual_timestamp # Update last seen time for playback
                self._active_aircraft_states[state.icao24] = state
                if state.icao24 not in self.flight_tracks_gui:
                    self.flight_tracks_gui[state.icao24] = deque(maxlen=self.max_track_points + 5)
                if state.latitude is not None and state.longitude is not None and state.timestamp is not None:
                    self.flight_tracks_gui[state.icao24].append((state.latitude, state.longitude, state.timestamp))
            
            self._current_flights_to_display_gui = list(self._active_aircraft_states.values())
        self._request_overlay_render()

    def get_current_map_info(self) -> Dict[str, Any]:
        size_w, size_h = (None, None)
        if PYPROJ_MODULE_LOCALLY_AVAILABLE and self._current_map_geo_bounds_gui:
            size_w, size_h = calculate_geographic_bbox_size_km(self._current_map_geo_bounds_gui) or (None, None)
        with self._map_data_lock:
            count = len(self._current_flights_to_display_gui)
            bbox = copy.deepcopy(self._target_bbox_input_gui)
        return {
            "zoom": self._current_zoom_gui, "map_geo_bounds": self._current_map_geo_bounds_gui,
            "target_bbox_input": bbox, "flight_count": count,
            "map_size_km_w": size_w, "map_size_km_h": size_h,
        }

    def shutdown_worker(self):
        self.map_render_manager.shutdown_worker()

    def recenter_map_at_coords(self, lat: float, lon: float):
        if self._current_zoom_gui is not None:
            self._request_base_map_render(lat, lon, self._current_zoom_gui)

    def set_bbox_around_coords(self, lat: float, lon: float, size_km: float):
        bbox_tuple = get_bounding_box_from_center_size(lat, lon, size_km)
        if bbox_tuple:
            self.set_target_bbox({"lon_min": bbox_tuple[0], "lat_min": bbox_tuple[1], "lon_max": bbox_tuple[2], "lat_max": bbox_tuple[3]})

    def zoom_in_at_center(self):
        if self._current_zoom_gui is not None and self._current_center_lat_gui is not None:
            self._request_base_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui + 1)

    def zoom_out_at_center(self):
        if self._current_zoom_gui is not None and self._current_center_lat_gui is not None:
            self._request_base_map_render(self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui - 1)

    def pan_map_fixed_step(self, direction: str):
        if not all([self._current_center_lat_gui, self._current_center_lon_gui, self._current_zoom_gui, PYPROJ_MODULE_LOCALLY_AVAILABLE]): return
        
        # Aggiungiamo questo controllo esplicito
        if not pyproj:
            logger.error("pyproj library not available, cannot perform pan operation.")
            return

        res = calculate_meters_per_pixel(self._current_center_lat_gui, self._current_zoom_gui, self.tile_manager.tile_size)
        if not res: return
        
        dx, dy = 0, 0
        if direction == "left": dx = -self.canvas_width * PAN_STEP_FRACTION
        elif direction == "right": dx = self.canvas_width * PAN_STEP_FRACTION
        elif direction == "up": dy = -self.canvas_height * PAN_STEP_FRACTION
        elif direction == "down": dy = self.canvas_height * PAN_STEP_FRACTION
        
        dmx, dmy = dx * res, -dy * res
        geod = pyproj.Geod(ellps="WGS84")
        new_lon, new_lat, _ = geod.fwd(self._current_center_lon_gui, self._current_center_lat_gui, 90, dmx)
        new_lon, new_lat, _ = geod.fwd(new_lon, new_lat, 0, dmy)
        self._request_base_map_render(new_lat, new_lon, self._current_zoom_gui)

    def center_map_and_fit_patch(self, lat: float, lon: float, size_km: float):
        zoom_w = calculate_zoom_level_for_geographic_size(lat, size_km * 1000, self.canvas_width, self.tile_manager.tile_size)
        zoom_h = calculate_zoom_level_for_geographic_size(lat, size_km * 1000, self.canvas_height, self.tile_manager.tile_size)
        zoom = min(zoom_w, zoom_h) - 1 if zoom_w and zoom_h else map_constants.DEFAULT_INITIAL_ZOOM
        self._request_base_map_render(lat, lon, max(map_constants.MIN_ZOOM_LEVEL, zoom))