"""
Class Camera Shared.
Keep the data between the modules.
Version: v0.1.13
"""

import asyncio
import logging
from typing import List

from PIL import Image

from ..const import (
    ALLOWED_IMAGE_FORMAT,
    ATTR_CALIBRATION_POINTS,
    ATTR_CAMERA_MODE,
    ATTR_CONTENT_TYPE,
    ATTR_DOCK_STATE,
    ATTR_IMAGE_LAST_UPDATED,
    ATTR_MARGINS,
    ATTR_OBSTACLES,
    ATTR_POINTS,
    ATTR_ROOMS,
    ATTR_ROTATE,
    ATTR_VACUUM_BATTERY,
    ATTR_VACUUM_CHARGING,
    ATTR_VACUUM_JSON_ID,
    ATTR_VACUUM_POSITION,
    ATTR_VACUUM_STATUS,
    ATTR_ZONES,
    CONF_ASPECT_RATIO,
    CONF_AUTO_ZOOM,
    CONF_OBSTACLE_LINK_IP,
    CONF_OBSTACLE_LINK_PORT,
    CONF_OBSTACLE_LINK_PROTOCOL,
    CONF_OFFSET_BOTTOM,
    CONF_OFFSET_LEFT,
    CONF_OFFSET_RIGHT,
    CONF_OFFSET_TOP,
    CONF_VAC_STAT,
    CONF_VAC_STAT_FONT,
    CONF_VAC_STAT_POS,
    CONF_VAC_STAT_SIZE,
    CONF_ZOOM_LOCK_RATIO,
    DEFAULT_VALUES,
    NOT_STREAMING_STATES,
)
from .types import (
    CameraModes,
    Colors,
    FloorData,
    PilPNG,
    TrimsData,
)
from .utils import pil_size_rotation


_LOGGER = logging.getLogger(__name__)


class CameraShared:
    """
    CameraShared class to keep the data between the classes.
    Implements a kind of Thread Safe data shared area.
    """

    def __init__(self, file_name):
        self.camera_mode: str = CameraModes.MAP_VIEW
        self.frame_number: int = 0
        self.destinations: list = []
        self.rand256_active_zone: list = []
        self.rand256_zone_coordinates: list = []
        self.is_rand: bool = False
        self.is_conga: bool = False
        self._new_mqtt_message = False
        self.last_image = Image.new("RGBA", (250, 150), (128, 128, 128, 255))
        self.new_image: PilPNG | None = None
        self.binary_image: bytes | None = None
        self.image_last_updated: float = 0.0
        self._image_format = "image/pil"
        self.image_size = None
        self.robot_size = 25
        self.mop_path_width = None
        self.image_auto_zoom: bool = False
        self.image_zoom_lock_ratio: bool = True
        self.image_ref_height: int = 0
        self.image_ref_width: int = 0
        self.image_aspect_ratio: str = "None"
        self.image_grab = True
        self.image_rotate: int = 0
        self.drawing_limit: float = 0.0
        self.current_room = None
        self.user_colors = Colors
        self.rooms_colors = Colors
        self.vacuum_battery = 0
        self.vacuum_connection = False
        self.vacuum_state = None
        self.dock_state = None
        self.charger_position = None
        self.show_vacuum_state = None
        self.vacuum_status_font: str = (
            "custom_components/mqtt_vacuum_camera/utils/fonts/FiraSans.ttf"
        )
        self.vacuum_status_size: int = 50
        self.vacuum_status_position: bool = True
        self.snapshot_take = False
        self.vacuum_error = None
        self.vacuum_api = None
        self.vacuum_ips = None
        self.vac_json_id = None
        self.obstacle_link_ip = None
        self.obstacle_link_port = None
        self.obstacle_link_protocol = None
        self.margins = "100"
        self.obstacles_data = None
        self.obstacles_pos = None
        self.offset_top = 0
        self.offset_down = 0
        self.offset_left = 0
        self.offset_right = 0
        self.export_svg = False
        self.svg_path = None
        self.enable_snapshots = False
        self.file_name = file_name
        self.attr_calibration_points = None
        self.map_rooms = None
        self.map_pred_zones = None
        self.map_pred_points = None
        self.map_new_path = None
        self.map_old_path = None
        self.user_language = None
        self.trim_crop_data = None
        self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"])
        self.floors_trims: dict[str, FloorData] = {}
        self.current_floor: str = "floor_0"
        self.skip_room_ids: List[str] = []
        self.device_info = None
        self.mop_mode: bool = False
        self._battery_state = None

    def vacuum_bat_charged(self) -> bool:
        """Check if the vacuum is charging."""
        if self.vacuum_state != "docked":
            self._battery_state = "not_charging"
        elif (self._battery_state == "charging_done") and (
            int(self.vacuum_battery) == 100
        ):
            self._battery_state = "charged"
        else:
            self._battery_state = (
                "charging" if int(self.vacuum_battery) < 100 else "charging_done"
            )
        return (self.vacuum_state == "docked") and (self._battery_state == "charging")

    def set_content_type(self, new_image_format: str = "pil") -> None:
        """Set image format / content type.

        Accepts either a short key ('pil', 'png', 'jpeg') or the full MIME
        value returned by content_type ('image/pil', 'image/png',
        'image/jpeg'), so that a round-trip set→get→set preserves the format.
        Unknown values fall back to 'image/pil'.
        """
        if new_image_format in ALLOWED_IMAGE_FORMAT:
            self._image_format = ALLOWED_IMAGE_FORMAT[new_image_format]
        elif new_image_format in ALLOWED_IMAGE_FORMAT.values():
            self._image_format = new_image_format
        else:
            self._image_format = ALLOWED_IMAGE_FORMAT["pil"]

    @property
    def get_content_type(self) -> str:
        """Return the current set _image_format"""
        return self._image_format

    @staticmethod
    def _compose_obstacle_links(
        vacuum_host_ip: str,
        obstacles: list,
        custom_ip: str | None = None,
        custom_port: int | None = None,
        custom_protocol: str | None = None,
    ) -> list | None:
        """
        Compose JSON with obstacle details including the image link.

        Args:
            vacuum_host_ip: Default vacuum host IP address
            obstacles: List of obstacle dictionaries
            custom_ip: Optional custom IP address for obstacle image links
            custom_port: Optional custom port for obstacle image links
            custom_protocol: Optional custom protocol (http/https) for obstacle image links

        Returns:
            List of obstacle dictionaries with image links, or None if no obstacles
        """
        obstacle_links = []
        if not obstacles or not vacuum_host_ip:
            return None

        # Determine which IP/port/protocol to use
        ip_to_use = custom_ip if custom_ip else vacuum_host_ip
        protocol_to_use = custom_protocol if custom_protocol else "http"

        # Build base URL
        if custom_port:
            base_url = f"{protocol_to_use}://{ip_to_use}:{custom_port}"
        else:
            base_url = f"{protocol_to_use}://{ip_to_use}"

        for obstacle in obstacles:
            label = obstacle.get("label", "")
            points = obstacle.get("points", {})
            image_id = obstacle.get("id", "None")

            if label and points and image_id:
                if image_id != "None":
                    image_link = (
                        f"{base_url}"
                        f"/api/v2/robot/capabilities/ObstacleImagesCapability/img/{image_id}"
                    )
                    obstacle_links.append(
                        {"point": points, "label": label, "link": image_link}
                    )
                else:
                    obstacle_links.append({"point": points, "label": label})
        return obstacle_links

    def update_user_colors(self, user_colors):
        """Update user colors palette"""
        self.user_colors = user_colors

    def get_user_colors(self):
        """Return user colors"""
        return self.user_colors

    def update_rooms_colors(self, user_colors):
        """Update the rooms colors."""
        self.rooms_colors = user_colors

    def get_rooms_colors(self):
        """Return rooms colors"""
        return self.rooms_colors

    def reset_trims(self) -> dict:
        """Reset the trims."""
        self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"])
        return self.trims

    def add_floor(self, floor_id: str, floor_data: FloorData):
        """Add or update a floor in floors_trims."""
        self.floors_trims[floor_id] = floor_data

    def update_floor(self, floor_id: str, new_trims: TrimsData):
        """Update trims for a specific floor."""
        if floor_id in self.floors_trims:
            self.floors_trims[floor_id].update_trims(new_trims)

    def remove_floor(self, floor_id: str):
        """Remove a floor from floors_trims."""
        if floor_id in self.floors_trims:
            del self.floors_trims[floor_id]

    async def batch_update(self, **kwargs):
        """Update the data of Shared in Batch"""
        for key, value in kwargs.items():
            setattr(self, key, value)

    async def batch_get(self, *args):
        """Batch get multiple attributes."""
        return {key: getattr(self, key) for key in args}

    def generate_attributes(self) -> dict:
        """Generate and return the shared attribute's dictionary."""
        attrs = {
            ATTR_IMAGE_LAST_UPDATED: self.image_last_updated,
            ATTR_CONTENT_TYPE: self._image_format,
            ATTR_VACUUM_JSON_ID: self.vac_json_id,
            ATTR_CAMERA_MODE: self.camera_mode,
            ATTR_VACUUM_STATUS: self.vacuum_state,
            ATTR_VACUUM_BATTERY: f"{self.vacuum_battery}%",
            ATTR_VACUUM_CHARGING: self.vacuum_bat_charged(),
            ATTR_VACUUM_POSITION: self.current_room,
            ATTR_CALIBRATION_POINTS: self.attr_calibration_points,
        }
        if self.dock_state is not None:
            attrs[ATTR_DOCK_STATE] = self.dock_state
        if self.obstacles_pos and self.vacuum_ips:
            self.obstacles_data = self._compose_obstacle_links(
                self.vacuum_ips,
                self.obstacles_pos,
                self.obstacle_link_ip,
                self.obstacle_link_port,
                self.obstacle_link_protocol,
            )
            attrs[ATTR_OBSTACLES] = self.obstacles_data

        shared_attrs = {
            ATTR_ROOMS: self.map_rooms,
            ATTR_ZONES: self.map_pred_zones,
            ATTR_POINTS: self.map_pred_points,
        }
        for key, value in shared_attrs.items():
            if value is not None:
                attrs[key] = value

        return attrs

    def is_streaming(self) -> bool:
        """Return true if the device is streaming."""
        updated_status = self.vacuum_state
        attr_is_streaming = (
            updated_status not in NOT_STREAMING_STATES or self.vacuum_bat_charged()
        ) or not self.binary_image
        return attr_is_streaming

    def to_dict(self) -> dict:
        """Return a dictionary with image and attributes data.

        Always includes image size information regardless of binary_image format.
        binary_image can be either PNG bytes or PIL Image object.
        """
        data = {
            "image": {
                "binary": self.binary_image,
                "size": pil_size_rotation(self.image_rotate, self.new_image),
                "streaming": self.is_streaming(),
            },
            "attributes": self.generate_attributes(),
        }
        return data


class CameraSharedManager:
    """Camera Shared Manager class."""

    def __init__(self, file_name: str, device_info: dict = None):
        self._instances = {}
        self._lock = asyncio.Lock()
        self.file_name = file_name
        if device_info:
            self.device_info = device_info
            self.update_shared_data(device_info)

    def update_shared_data(self, device_info):
        """Initialize the shared data with device_info."""
        instance = self.get_instance()  # Retrieve the correct instance

        try:
            # Store the device_info in the instance
            instance.device_info = device_info
            _LOGGER.debug(
                "%s: Stored device_info in shared instance", instance.file_name
            )

            instance.attr_calibration_points = None

            # Initialize shared data with defaults from DEFAULT_VALUES
            instance.offset_top = int(
                device_info.get(CONF_OFFSET_TOP, DEFAULT_VALUES["offset_top"])
            )
            instance.offset_down = int(
                device_info.get(CONF_OFFSET_BOTTOM, DEFAULT_VALUES["offset_bottom"])
            )
            instance.offset_left = int(
                device_info.get(CONF_OFFSET_LEFT, DEFAULT_VALUES["offset_left"])
            )
            instance.offset_right = int(
                device_info.get(CONF_OFFSET_RIGHT, DEFAULT_VALUES["offset_right"])
            )
            instance.image_auto_zoom = device_info.get(
                CONF_AUTO_ZOOM, DEFAULT_VALUES["auto_zoom"]
            )
            instance.image_zoom_lock_ratio = device_info.get(
                CONF_ZOOM_LOCK_RATIO, DEFAULT_VALUES["zoom_lock_ratio"]
            )
            instance.image_aspect_ratio = device_info.get(
                CONF_ASPECT_RATIO, DEFAULT_VALUES["aspect_ratio"]
            )
            instance.image_rotate = int(
                device_info.get(ATTR_ROTATE, DEFAULT_VALUES["rotate_image"])
            )
            instance.margins = int(
                device_info.get(ATTR_MARGINS, DEFAULT_VALUES["margins"])
            )
            instance.show_vacuum_state = device_info.get(
                CONF_VAC_STAT, DEFAULT_VALUES["show_vac_status"]
            )
            instance.vacuum_status_font = device_info.get(
                CONF_VAC_STAT_FONT, DEFAULT_VALUES["vac_status_font"]
            )
            instance.vacuum_status_size = int(
                device_info.get(CONF_VAC_STAT_SIZE, DEFAULT_VALUES["vac_status_size"])
            )
            instance.vacuum_status_position = device_info.get(
                CONF_VAC_STAT_POS, DEFAULT_VALUES["vac_status_position"]
            )
            instance.set_content_type(
                device_info.get("def_context_type", "pil")
            )
            # Robot size
            robot_size = device_info.get("robot_size", 25)
            try:
                robot_size = int(robot_size)
            except (ValueError, TypeError):
                robot_size = 25
            # Clamp robot_size to [8, 25]
            if robot_size < 8:
                robot_size = 8
            elif robot_size > 25:
                robot_size = 25
            instance.robot_size = robot_size
            # Mop path width - ensure it's an integer
            mop_path_width = device_info.get("mop_path_width", robot_size - 2)
            try:
                instance.mop_path_width = int(mop_path_width)
            except (ValueError, TypeError):
                instance.mop_path_width = robot_size - 2

            # Obstacle link configuration
            instance.obstacle_link_ip = device_info.get(CONF_OBSTACLE_LINK_IP)
            instance.obstacle_link_protocol = device_info.get(
                CONF_OBSTACLE_LINK_PROTOCOL
            )
            obstacle_link_port = device_info.get(CONF_OBSTACLE_LINK_PORT)
            if obstacle_link_port is not None:
                try:
                    instance.obstacle_link_port = int(obstacle_link_port)
                except (ValueError, TypeError):
                    instance.obstacle_link_port = None
            else:
                instance.obstacle_link_port = None

            # Check for new floors_data first
            floors_data = device_info.get("floors_data", {})
            current_floor = device_info.get(
                "current_floor", "floor_0"
            )  # Default fallback

            if floors_data:
                # NEW: Use floors_data
                floor_data = floors_data.get(
                    current_floor, DEFAULT_VALUES["trims_data"]
                )

                # Handle FloorData object vs dict format
                if isinstance(floor_data, FloorData):
                    # FloorData object - check rotation and extract data
                    saved_rotation = floor_data.rotation
                    map_name = floor_data.map_name

                    # Check if rotation has changed from saved value
                    if saved_rotation != instance.image_rotate:
                        # Rotation changed - reset trims to defaults and let auto-crop recalculate
                        _LOGGER.debug(
                            "%s: Rotation changed from %d° to %d° - resetting trims for recalculation",
                            self.file_name,
                            saved_rotation,
                            instance.image_rotate,
                        )
                        instance.trims = TrimsData.from_dict(
                            DEFAULT_VALUES["trims_data"]
                        )
                    else:
                        # Rotation matches - use saved trims
                        instance.trims = floor_data.trims
                elif isinstance(floor_data, dict) and "trims" in floor_data:
                    # FloorData.to_dict() format - extract trims sub-dict
                    floor_trims = floor_data["trims"]
                    map_name = floor_data.get("map_name", "")
                    saved_rotation = floor_data.get("rotation", 0)

                    # Check if rotation has changed from saved value
                    if saved_rotation != instance.image_rotate:
                        # Rotation changed - reset trims to defaults and let auto-crop recalculate
                        _LOGGER.debug(
                            "%s: Rotation changed from %d° to %d° - resetting trims for recalculation",
                            self.file_name,
                            saved_rotation,
                            instance.image_rotate,
                        )
                        instance.trims = TrimsData.from_dict(
                            DEFAULT_VALUES["trims_data"]
                        )
                    else:
                        # Rotation matches - use saved trims
                        instance.trims = TrimsData.from_dict(floor_trims)
                else:
                    # Legacy format - floor_data is already the trims dict
                    floor_trims = floor_data
                    map_name = ""
                    instance.trims = TrimsData.from_dict(floor_trims)

                instance.current_floor = current_floor
                # Store map_name if available (for rand256 operations)
                if hasattr(instance, "map_name"):
                    instance.map_name = map_name
            else:
                # OLD: Backward compatibility
                trim_data = device_info.get("trims_data", DEFAULT_VALUES["trims_data"])
                instance.trims = TrimsData.from_dict(trim_data)
                instance.current_floor = "floor_0"  # Default

        except TypeError as ex:
            _LOGGER.warning(
                "Shared data can't be initialized due to a TypeError! %s", ex
            )
        except AttributeError as ex:
            _LOGGER.warning(
                "Shared data can't be initialized due to an AttributeError! %s", ex
            )
        except RuntimeError as ex:
            _LOGGER.warning(
                "An unexpected error occurred while initializing shared data %s:", ex
            )

    def get_instance(self):
        """Get the shared instance."""
        if self.file_name not in self._instances:
            self._instances[self.file_name] = CameraShared(self.file_name)
            self._instances[self.file_name].file_name = self.file_name
        return self._instances[self.file_name]

    async def update_instance(self, **kwargs):
        """Update the shared instance."""
        async with self._lock:
            instance = self.get_instance()
            await instance.batch_update(**kwargs)
