"""Storage management for Climate Scheduler."""
import logging
import copy
import re
from typing import Any, Dict, List, Optional
from datetime import datetime, time

from homeassistant.core import HomeAssistant
from homeassistant.helpers.storage import Store
from homeassistant.const import UnitOfTemperature

from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, DEFAULT_SCHEDULE, MIN_TEMP, MAX_TEMP

_LOGGER = logging.getLogger(__name__)


def validate_node(node: Dict[str, Any]) -> bool:
    """Validate a schedule node structure."""
    if not isinstance(node, dict):
        return False
    
    # Check required fields
    if "time" not in node or "temp" not in node:
        _LOGGER.error(f"Node missing required fields: {node}")
        return False
    
    # Validate time format (HH:MM)
    time_str = node["time"]
    if not isinstance(time_str, str) or len(time_str) != 5 or time_str[2] != ":":
        _LOGGER.error(f"Invalid time format: {time_str}")
        return False
    
    try:
        hours, minutes = time_str.split(":")
        h, m = int(hours), int(minutes)
        # Normalize 24:00 to 23:59 to avoid clash with 00:00 on next day
        if h == 24 and m == 0:
            h, m = 23, 59
        if not (0 <= h <= 23 and 0 <= m <= 59):
            _LOGGER.error(f"Time out of range: {time_str}")
            return False
    except (ValueError, AttributeError):
        _LOGGER.error(f"Cannot parse time: {time_str}")
        return False
    
    # Validate temperature is numeric
    try:
        float(node["temp"])
    except (ValueError, TypeError):
        _LOGGER.error(f"Invalid temperature: {node.get('temp')}")
        return False
    
    return True


class ScheduleStorage:
    """Handle storage of heating schedules."""

    def __init__(self, hass: HomeAssistant) -> None:
        """Initialize storage."""
        self.hass = hass
        self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
        self._data: Dict[str, Any] = {}

    async def async_load(self) -> None:
        """Load data from storage."""
        data = await self._store.async_load()
        if data is None:
            self._data = {"groups": {}, "settings": {}, "advance_history": {}}
        else:
            self._data = data
            changed_settings = False

            # Collect nested settings layers (if any) by following repeated
            # {"settings": {...}} wrappers.
            #
            # This repository has had a settings-shape mismatch where the UI
            # sometimes treated {settings: {...}, version: {...}} as the actual
            # settings dict. That can lead to recursive nesting like:
            #   data.settings.settings.settings...
            #
            # On load we:
            # - gather all layers
            # - select the layer with the newest parsed integration version
            # - flatten to a single dict (dropping nested "settings" and "version")
            # - backfill any missing keys from other layers (to avoid losing
            #   settings that only exist in older layers).
            if "settings" in self._data and isinstance(self._data.get("settings"), dict):
                layers: List[Dict[str, Any]] = []
                s: Any = self._data["settings"]

                while isinstance(s, dict):
                    layers.append(s)
                    if "settings" in s and isinstance(s.get("settings"), dict):
                        s = s["settings"]
                    else:
                        break

                if len(layers) > 1:
                    changed_settings = True

                def parse_version_string(ver: Any) -> tuple:
                    if not isinstance(ver, str):
                        return ()
                    # Extract numeric groups from version string, e.g. '1.14.0.13' -> (1,14,0,13)
                    nums = re.findall(r"\d+", ver)
                    return tuple(int(x) for x in nums) if nums else ()

                def layer_version(layer: Dict[str, Any]) -> tuple:
                    v = layer.get("version")
                    if isinstance(v, dict):
                        return parse_version_string(v.get("integration") or v.get("version"))
                    return parse_version_string(v)

                best_layer = layers[-1]  # default: innermost
                best_ver = ()
                for layer in layers:
                    parsed = layer_version(layer)
                    if parsed and parsed > best_ver:
                        best_ver = parsed
                        best_layer = layer

                # Start with the newest layer as the source of truth.
                cleaned_settings: Dict[str, Any] = {
                    k: v
                    for k, v in best_layer.items()
                    if k not in {"settings", "version", "performance_tracking"}
                }

                # Backfill missing keys from any layer (newest doesn't always
                # mean it contains all fields if the nesting was corrupted).
                for layer in reversed(layers):  # inner -> outer
                    for k, v in layer.items():
                        if k in {"settings", "version", "performance_tracking"}:
                            continue
                        if k not in cleaned_settings:
                            cleaned_settings[k] = v

                self._data["settings"] = cleaned_settings
            # Ensure groups key exists for backwards compatibility
            if "groups" not in self._data:
                self._data["groups"] = {}
            # Ensure settings exist
            if "settings" not in self._data:
                self._data["settings"] = {}
            # Ensure advance_history exists
            if "advance_history" not in self._data:
                self._data["advance_history"] = {}
            # Migrate old single-schedule format to new day-based format
            await self._migrate_to_day_schedules()
            # Migrate to profile-based structure
            await self._migrate_to_profiles()
            # Migrate entities to single-entity groups (will remove entities key after migration)
            await self._migrate_entities_to_groups()
            # Migrate per-group profiles to global profile registry
            await self._migrate_profiles_to_global()
            # Normalize older tagged legacy profile names to metadata format
            await self._migrate_legacy_profile_name_suffixes()
        # Ensure global profile structure exists and schedule/profile views are synchronized
        self._ensure_global_profiles_initialized()
        self._sync_group_profile_views()
        # Ensure min/max temp defaults are present in settings
        settings = self._data.get("settings", {})
        # Drop legacy/unreferenced settings keys that may have been persisted
        # due to older settings payload shape mismatches.
        if isinstance(settings, dict) and "performance_tracking" in settings:
            del settings["performance_tracking"]
            changed_settings = True
        if "min_temp" not in settings:
            # Set default based on temperature unit
            if self.hass.config.units.temperature_unit == UnitOfTemperature.FAHRENHEIT:
                settings["min_temp"] = 42.0  # Fahrenheit
            else:
                settings["min_temp"] = 5.0  # Celsius
        if "max_temp" not in settings:
            # Set default based on temperature unit
            if self.hass.config.units.temperature_unit == UnitOfTemperature.FAHRENHEIT:
                settings["max_temp"] = 86.0  # Fahrenheit
            else:
                settings["max_temp"] = 30.0  # Celsius
        if "create_derivative_sensors" not in settings:
            settings["create_derivative_sensors"] = True  # Default to enabled
        if "graph_type" not in settings:
            settings["graph_type"] = "svg"  # Default to SVG graph (classic)
        self._data["settings"] = settings
        # Persist cleaned settings if we flattened/cleaned nested layers
        if locals().get("changed_settings"):
            try:
                await self.async_save()
                _LOGGER.info("Persisted cleaned settings to storage")
            except Exception as e:
                _LOGGER.error(f"Failed to persist cleaned settings: {e}")
        _LOGGER.debug(f"Loaded schedule data: {self._data}")
    
    async def _migrate_to_day_schedules(self) -> None:
        """Migrate existing schedules to day-based format."""
        migrated = False
        for entity_id, entity_data in self._data.get("entities", {}).items():
            # Check if already in new format (has schedule_mode key)
            if "schedule_mode" not in entity_data:
                # Migrate from old format
                old_nodes = entity_data.get("nodes", [])
                entity_data["schedule_mode"] = "all_days"  # Default to all days
                entity_data["schedules"] = {
                    "all_days": old_nodes
                }
                # Remove old nodes key
                if "nodes" in entity_data:
                    del entity_data["nodes"]
                migrated = True
                _LOGGER.info(f"Migrated {entity_id} to day-based schedule format")
        
        # Migrate groups
        for group_name, group_data in self._data.get("groups", {}).items():
            if "schedule_mode" not in group_data:
                old_nodes = group_data.get("nodes", [])
                group_data["schedule_mode"] = "all_days"
                group_data["schedules"] = {
                    "all_days": old_nodes
                }
                if "nodes" in group_data:
                    del group_data["nodes"]
                migrated = True
                _LOGGER.info(f"Migrated group '{group_name}' to day-based schedule format")
        
        if migrated:
            await self.async_save()
    
    async def _migrate_to_profiles(self) -> None:
        """Migrate existing schedules to profile-based format."""
        migrated = False
        
        # Migrate entities
        for entity_id, entity_data in self._data.get("entities", {}).items():
            if "profiles" not in entity_data or "active_profile" not in entity_data:
                # Create Default profile from current schedule
                schedule_mode = entity_data.get("schedule_mode", "all_days")
                schedules = copy.deepcopy(entity_data.get("schedules", {"all_days": []}))
                
                entity_data["profiles"] = {
                    "Default": {
                        "schedule_mode": schedule_mode,
                        "schedules": schedules
                    }
                }
                entity_data["active_profile"] = "Default"
                migrated = True
                _LOGGER.info(f"Migrated entity {entity_id} to profile-based format")
        
        # Migrate groups
        for group_name, group_data in self._data.get("groups", {}).items():
            if "profiles" not in group_data or "active_profile" not in group_data:
                # Create Default profile from current schedule
                schedule_mode = group_data.get("schedule_mode", "all_days")
                schedules = copy.deepcopy(group_data.get("schedules", {"all_days": []}))
                
                group_data["profiles"] = {
                    "Default": {
                        "schedule_mode": schedule_mode,
                        "schedules": schedules
                    }
                }
                group_data["active_profile"] = "Default"
                migrated = True
                _LOGGER.info(f"Migrated group '{group_name}' to profile-based format")
        
        if migrated:
            await self.async_save()
    
    async def _migrate_entities_to_groups(self) -> None:
        """Migrate individual entities to single-entity groups for unified backend."""
        migrated = False
        
        if "groups" not in self._data:
            self._data["groups"] = {}
        
        # Migrate individual entities to single-entity groups
        for entity_id, entity_data in list(self._data.get("entities", {}).items()):
            # Check if entity is already in a multi-entity group
            entity_in_group = False
            for group_name, group_data in self._data["groups"].items():
                if entity_id in group_data.get("entities", []):
                    entity_in_group = True
                    break
            
            # If not in a group, create a single-entity group
            if not entity_in_group:
                # Use entity_id as the group name (with a prefix to distinguish)
                group_name = f"__entity_{entity_id}"
                
                # Only create if it doesn't already exist
                if group_name not in self._data["groups"]:
                    self._data["groups"][group_name] = {
                        "entities": [entity_id],
                        "enabled": entity_data.get("enabled", True),
                        "ignored": entity_data.get("ignored", False),  # Preserve ignored status
                        "schedule_mode": entity_data.get("schedule_mode", "all_days"),
                        "schedules": copy.deepcopy(entity_data.get("schedules", {"all_days": []})),
                        "profiles": copy.deepcopy(entity_data.get("profiles", {
                            "Default": {
                                "schedule_mode": "all_days",
                                "schedules": {"all_days": []}
                            }
                        })),
                        "active_profile": entity_data.get("active_profile", "Default"),
                        "_is_single_entity_group": True  # Internal marker
                    }
                    migrated = True
                    _LOGGER.info(f"Migrated entity {entity_id} to single-entity group '{group_name}'")
        
        # Remove the legacy entities structure after migration
        if "entities" in self._data and self._data["entities"]:
            _LOGGER.info(f"Removing legacy entities structure after migration to single-entity groups")
            del self._data["entities"]
            migrated = True
        
        if migrated:
            await self.async_save()

    async def async_save(self) -> None:
        """Save data to storage."""
        self._sync_group_profile_views()
        await self._store.async_save(self._data)
        _LOGGER.debug(f"Saved schedule data: {self._data}")

    def _get_default_schedule_template(self) -> List[Dict[str, Any]]:
        """Return configured default schedule template with validation fallback."""
        settings = self._data.get("settings", {})
        configured = settings.get("defaultSchedule") or settings.get("default_schedule")

        if isinstance(configured, list) and configured:
            valid_nodes: List[Dict[str, Any]] = []
            for node in configured:
                if validate_node(node):
                    valid_nodes.append(copy.deepcopy(node))

            if valid_nodes:
                return valid_nodes

        return copy.deepcopy(DEFAULT_SCHEDULE)

    def _build_schedules_from_template(self, default_schedule: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
        """Build schedule dictionary from the single default schedule template."""
        return {"all_days": copy.deepcopy(default_schedule)}

    def _ensure_global_profiles_initialized(self) -> None:
        """Ensure global profiles exist with at least one usable profile."""
        profiles = self._data.get("profiles")
        if not isinstance(profiles, dict):
            profiles = {}

        if not profiles:
            default_schedule = self._get_default_schedule_template()
            profiles["Default"] = {
                "schedule_mode": "all_days",
                "schedules": self._build_schedules_from_template(default_schedule)
            }

        self._data["profiles"] = profiles

    def _resolve_group_active_global_profile(self, group_data: Dict[str, Any], global_profiles: Dict[str, Any]) -> Optional[str]:
        """Resolve the active global profile for a group using compatibility fields."""
        active_global = group_data.get("active_profile_global")
        if active_global in global_profiles:
            return active_global

        active_profile = group_data.get("active_profile")
        if active_profile in global_profiles:
            return active_profile

        return "Default" if "Default" in global_profiles else next(iter(global_profiles), None)

    def _sync_group_profile_views(self) -> None:
        """Align active schedule fields with selected global active profile."""
        groups = self._data.get("groups", {})
        if not isinstance(groups, dict):
            return

        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})
        for group_data in groups.values():
            if not isinstance(group_data, dict):
                continue

            active_profile = self._resolve_group_active_global_profile(group_data, global_profiles)
            if active_profile:
                group_data["active_profile_global"] = active_profile

            if active_profile:
                active_profile_data = global_profiles.get(active_profile, {})
                group_data["schedule_mode"] = active_profile_data.get("schedule_mode", "all_days")
                group_data["schedules"] = copy.deepcopy(active_profile_data.get("schedules", {"all_days": []}))

    async def _migrate_profiles_to_global(self) -> None:
        """Migrate legacy per-group profile dictionaries into global profiles."""
        groups = self._data.get("groups", {})
        if not isinstance(groups, dict):
            return

        global_profiles = self._data.get("profiles")
        if not isinstance(global_profiles, dict):
            global_profiles = {}

        migrated = False

        def make_unique_profile_name(base_name: str) -> str:
            candidate = base_name
            suffix = 2
            while candidate in global_profiles:
                candidate = f"{base_name} ({suffix})"
                suffix += 1
            return candidate

        if not global_profiles:
            for group_name, group_data in groups.items():
                if not isinstance(group_data, dict):
                    continue

                legacy_profiles = group_data.get("profiles", {})
                active_profile = group_data.get("active_profile", "Default")

                preserved_legacy_profiles: Dict[str, Any] = {}

                if isinstance(legacy_profiles, dict) and legacy_profiles:
                    name_map: Dict[str, str] = {}
                    for profile_name, profile_data in legacy_profiles.items():
                        source_profile = profile_data if isinstance(profile_data, dict) else {}
                        new_profile_name = make_unique_profile_name(f"{group_name} - {profile_name}")
                        name_map[profile_name] = new_profile_name
                        global_profiles[new_profile_name] = {
                            "schedule_mode": source_profile.get("schedule_mode", group_data.get("schedule_mode", "all_days")),
                            "schedules": copy.deepcopy(source_profile.get("schedules", group_data.get("schedules", {"all_days": []})))
                        }

                        preserved_legacy_profiles[profile_name] = {
                            "schedule_mode": source_profile.get("schedule_mode", group_data.get("schedule_mode", "all_days")),
                            "schedules": copy.deepcopy(source_profile.get("schedules", group_data.get("schedules", {"all_days": []}))),
                            "legacy": True,
                        }

                    group_data["profiles"] = preserved_legacy_profiles
                    if active_profile in preserved_legacy_profiles:
                        group_data["active_profile_legacy"] = active_profile
                        group_data["active_profile"] = active_profile

                    mapped_active_profile = name_map.get(active_profile)
                    if mapped_active_profile:
                        group_data["active_profile_global"] = mapped_active_profile
                    elif name_map:
                        group_data["active_profile_global"] = next(iter(name_map.values()))
                else:
                    fallback_name = make_unique_profile_name(f"{group_name} - Default")
                    global_profiles[fallback_name] = {
                        "schedule_mode": group_data.get("schedule_mode", "all_days"),
                        "schedules": copy.deepcopy(group_data.get("schedules", {"all_days": []}))
                    }

                    legacy_default_name = "Default"
                    preserved_legacy_profiles[legacy_default_name] = {
                        "schedule_mode": group_data.get("schedule_mode", "all_days"),
                        "schedules": copy.deepcopy(group_data.get("schedules", {"all_days": []})),
                        "legacy": True,
                    }
                    group_data["profiles"] = preserved_legacy_profiles
                    group_data["active_profile_legacy"] = legacy_default_name
                    group_data["active_profile"] = legacy_default_name
                    group_data["active_profile_global"] = fallback_name

            migrated = True

        if migrated:
            self._data["profiles"] = global_profiles
            self._sync_group_profile_views()
            await self.async_save()
            _LOGGER.info("Migrated legacy per-group profiles to global profiles")

    async def _migrate_legacy_profile_name_suffixes(self) -> None:
        """Normalize legacy profile names from '<name> [legacy]' to '<name>' with metadata."""
        groups = self._data.get("groups", {})
        if not isinstance(groups, dict):
            return

        suffix = " [legacy]"
        migrated = False

        for group_data in groups.values():
            if not isinstance(group_data, dict):
                continue

            profiles = group_data.get("profiles", {})
            if not isinstance(profiles, dict) or not profiles:
                continue

            normalized_profiles: Dict[str, Any] = {}
            name_map: Dict[str, str] = {}
            group_changed = False

            for profile_name, profile_data in profiles.items():
                target_name = profile_name
                source_profile = profile_data if isinstance(profile_data, dict) else {}

                if isinstance(profile_name, str) and profile_name.endswith(suffix):
                    base_name = profile_name[: -len(suffix)]
                    candidate_name = base_name
                    if candidate_name in normalized_profiles:
                        # Avoid accidental overwrite if both names exist; keep original key.
                        candidate_name = profile_name
                    else:
                        group_changed = True

                    target_name = candidate_name

                normalized_profile = copy.deepcopy(source_profile)
                if profile_name != target_name and normalized_profile.get("legacy") is not True:
                    normalized_profile["legacy"] = True
                    group_changed = True

                if target_name in normalized_profiles:
                    # Collision fallback (non-suffix duplicate): preserve existing and keep incoming key.
                    target_name = profile_name
                    normalized_profile = copy.deepcopy(source_profile)

                if target_name != profile_name:
                    name_map[profile_name] = target_name

                normalized_profiles[target_name] = normalized_profile

            if group_changed:
                group_data["profiles"] = normalized_profiles

                for key in ("active_profile", "active_profile_legacy"):
                    current_value = group_data.get(key)
                    if isinstance(current_value, str) and current_value in name_map:
                        group_data[key] = name_map[current_value]

                migrated = True

        if migrated:
            await self.async_save()
            _LOGGER.info("Normalized legacy profile name suffixes to metadata format")

    async def async_get_settings(self) -> Dict[str, Any]:
        """Return current global settings."""
        return self._data.get("settings", {})

    async def async_save_settings(self, settings: Dict[str, Any]) -> None:
        """Save global settings to storage by merging and persisting."""
        if "settings" not in self._data:
            self._data["settings"] = {}
        # Merge provided settings
        for k, v in settings.items():
            self._data["settings"][k] = v
        await self.async_save()
        _LOGGER.debug(f"Saved settings: {self._data.get('settings')}")

    async def async_get_advance_history(self) -> Dict[str, Any]:
        """Return advance history for all entities."""
        return self._data.get("advance_history", {})

    async def async_save_advance_history(self, history: Dict[str, Any]) -> None:
        """Save advance history to storage."""
        self._data["advance_history"] = history
        await self.async_save()
        _LOGGER.debug(f"Saved advance history: {history}")
    
    async def async_factory_reset(self) -> None:
        """Reset all data to factory defaults (freshly installed state)."""
        _LOGGER.warning("Factory reset requested - clearing all schedules, groups, and settings")
        
        # Reset to fresh state with default settings
        self._data = {
            "entities": {},
            "groups": {},
            "settings": {},
            "advance_history": {}
        }
        
        # Set default min/max temp based on temperature unit
        from homeassistant.const import UnitOfTemperature
        if self.hass.config.units.temperature_unit == UnitOfTemperature.FAHRENHEIT:
            self._data["settings"]["min_temp"] = 42.0  # Fahrenheit
            self._data["settings"]["max_temp"] = 86.0  # Fahrenheit
        else:
            self._data["settings"]["min_temp"] = 5.0  # Celsius
            self._data["settings"]["max_temp"] = 30.0  # Celsius
        
        # Set default for derivative sensors
        self._data["settings"]["create_derivative_sensors"] = True
        
        await self.async_save()
        _LOGGER.info("Factory reset completed - all data cleared and defaults restored")

    def _find_single_entity_group(self, entity_id: str) -> Optional[str]:
        """Find the single-entity group name for an entity (checks both old __entity_ format and friendly name format)."""
        # Check old format first
        old_format = f"__entity_{entity_id}"
        if old_format in self._data.get("groups", {}):
            group_data = self._data["groups"][old_format]
            if group_data.get("_is_single_entity_group") and entity_id in group_data.get("entities", []):
                return old_format
        
        # Check all groups for single-entity groups containing this entity
        for group_name, group_data in self._data.get("groups", {}).items():
            if (group_data.get("_is_single_entity_group") and 
                len(group_data.get("entities", [])) == 1 and 
                entity_id in group_data.get("entities", [])):
                return group_name
        
        return None

    async def async_get_all_entities(self) -> List[str]:
        """Get list of all entity IDs with schedules (from single-entity groups)."""
        entity_ids = []
        for group_name, group_data in self._data.get("groups", {}).items():
            # Extract entity IDs from all groups (single and multi-entity)
            entity_ids.extend(group_data.get("entities", []))
        return list(set(entity_ids))  # Remove duplicates

    async def async_get_schedule(self, entity_id: str, day: Optional[str] = None) -> Optional[Dict[str, Any]]:
        """Get schedule for an entity (from its single-entity group or multi-entity group). If day is specified, returns nodes for that day."""
        # Check if entity is in a single-entity group
        single_group_name = self._find_single_entity_group(entity_id)
        if single_group_name:
            group_data = self._data["groups"][single_group_name]
            projected_group = self._project_group_runtime_view(group_data)
            _LOGGER.debug(f"async_get_schedule: entity {entity_id} found in single-entity group - enabled={group_data.get('enabled', True)}")
            
            # If no day specified, return the whole schedule structure
            if day is None:
                return projected_group
            
            # Return nodes for specific day based on schedule mode
            return {
                "nodes": self._get_nodes_for_day(projected_group, day),
                "enabled": group_data.get("enabled", True),
                "schedule_mode": projected_group.get("schedule_mode", "all_days")
            }
        
        # Check if entity is in a multi-entity group
        for group_name, group_data in self._data.get("groups", {}).items():
            if entity_id in group_data.get("entities", []):
                projected_group = self._project_group_runtime_view(group_data)
                _LOGGER.debug(f"async_get_schedule: entity {entity_id} found in multi-entity group '{group_name}' - enabled={group_data.get('enabled', True)}")
                
                # If no day specified, return the whole schedule structure
                if day is None:
                    return projected_group
                
                # Return nodes for specific day based on schedule mode
                return {
                    "nodes": self._get_nodes_for_day(projected_group, day),
                    "enabled": group_data.get("enabled", True),
                    "schedule_mode": projected_group.get("schedule_mode", "all_days")
                }
        
        _LOGGER.debug(f"async_get_schedule: entity {entity_id} not found in any group")
        return None
    
    def _get_nodes_for_day(self, entity_data: Dict[str, Any], day: str) -> List[Dict[str, Any]]:
        """Get nodes for a specific day based on schedule mode."""
        schedule_mode = entity_data.get("schedule_mode", "all_days")
        schedules = entity_data.get("schedules", {})
        
        if schedule_mode == "all_days":
            return schedules.get("all_days", [])
        elif schedule_mode == "5/2":
            # If day is already weekday/weekend, use it directly
            if day in ["weekday", "weekend"]:
                return schedules.get(day, [])
            # Otherwise map individual days to weekday/weekend
            # Weekdays: mon, tue, wed, thu, fri -> "weekday"
            # Weekends: sat, sun -> "weekend"
            elif day in ["mon", "tue", "wed", "thu", "fri"]:
                return schedules.get("weekday", [])
            else:
                return schedules.get("weekend", [])
        elif schedule_mode == "individual":
            return schedules.get(day, [])
        
        return []

    async def async_set_schedule(self, entity_id: str, nodes: List[Dict[str, Any]], day: Optional[str] = None, schedule_mode: Optional[str] = None) -> None:
        """Set schedule nodes for an entity by creating/updating its single-entity group."""
        # Check if entity is in a multi-entity group
        for group_name, group_data in self._data.get("groups", {}).items():
            if not group_data.get("_is_single_entity_group", False) and entity_id in group_data.get("entities", []):
                # Entity is in a real group, update the group schedule instead
                _LOGGER.info(f"Entity {entity_id} is in group '{group_name}', updating group schedule")
                await self.async_set_group_schedule(group_name, nodes, day, schedule_mode)
                return
        
        # Check if entity already has a single-entity group (old or new format)
        single_group_name = self._find_single_entity_group(entity_id)
        
        # If no existing group, create new one with friendly name
        if not single_group_name:
            friendly_name = entity_id
            if state := self.hass.states.get(entity_id):
                friendly_name = state.attributes.get("friendly_name", entity_id)
            single_group_name = friendly_name
        
        # Ensure groups dict exists
        if "groups" not in self._data:
            self._data["groups"] = {}
        
        # Create group if it doesn't exist
        if single_group_name not in self._data["groups"]:
            self._data["groups"][single_group_name] = {
                "entities": [entity_id],
                "enabled": True,
                "ignored": False,
                "schedule_mode": schedule_mode or "all_days",
                "schedules": {},
                "profiles": {
                    "Default": {
                        "schedule_mode": schedule_mode or "all_days",
                        "schedules": {}
                    }
                },
                "active_profile": "Default",
                "_is_single_entity_group": True
            }
            _LOGGER.info(f"Created single-entity group '{single_group_name}' for {entity_id}")
        
        # Now update the group schedule
        await self.async_set_group_schedule(single_group_name, nodes, day, schedule_mode)

    async def async_add_entity(self, entity_id: str) -> None:
        """Add a new entity with default schedule by creating a single-entity group."""
        # Get friendly name for the group
        friendly_name = entity_id
        if state := self.hass.states.get(entity_id):
            friendly_name = state.attributes.get("friendly_name", entity_id)
        single_group_name = friendly_name
        
        if "groups" not in self._data:
            self._data["groups"] = {}
        
        if single_group_name not in self._data["groups"]:
            self._data["groups"][single_group_name] = {
                "entities": [entity_id],
                "enabled": True,
                "ignored": False,
                "schedule_mode": "all_days",
                "schedules": {
                    "all_days": DEFAULT_SCHEDULE.copy()
                },
                "profiles": {
                    "Default": {
                        "schedule_mode": "all_days",
                        "schedules": {
                            "all_days": DEFAULT_SCHEDULE.copy()
                        }
                    }
                },
                "active_profile": "Default",
                "_is_single_entity_group": True
            }
            await self.async_save()
            _LOGGER.info(f"Added entity {entity_id} with default schedule as single-entity group")

    async def async_remove_entity(self, entity_id: str) -> None:
        """Remove an entity and its schedule (single-entity group)."""
        # Remove the single-entity group
        single_group_name = self._find_single_entity_group(entity_id)
        if single_group_name and single_group_name in self._data.get("groups", {}):
            del self._data["groups"][single_group_name]
            await self.async_save()
            _LOGGER.info(f"Removed single-entity group '{single_group_name}' for entity {entity_id}")
        else:
            _LOGGER.warning(f"Attempted to remove entity {entity_id} but its single-entity group doesn't exist")
    
    async def async_set_ignored(self, entity_id: str, ignored: bool) -> None:
        """Set whether an entity should be ignored (not monitored)."""
        _LOGGER.info(f"async_set_ignored called: entity_id={entity_id}, ignored={ignored}")
        
        # Find which group this entity belongs to
        entity_group_name = None
        for group_name, group_data in self._data.get("groups", {}).items():
            if entity_id in group_data.get("entities", []):
                entity_group_name = group_name
                break
        
        # Update the group if the entity is in one
        if entity_group_name:
            group_data = self._data["groups"][entity_group_name]
            group_data["ignored"] = ignored
            # If ignored, disable the group; if not ignored, enable it
            if ignored:
                group_data["enabled"] = False
            else:
                group_data["enabled"] = True

                schedules = group_data.get("schedules", {})
                has_any_nodes = (
                    isinstance(schedules, dict)
                    and any(isinstance(nodes, list) and len(nodes) > 0 for nodes in schedules.values())
                )

                if not has_any_nodes:
                    default_schedule = self._get_default_schedule_template()
                    group_data["schedule_mode"] = "all_days"
                    group_data["schedules"] = self._build_schedules_from_template(default_schedule)

                    profiles = group_data.get("profiles", {})
                    if not isinstance(profiles, dict):
                        profiles = {}
                    if "Default" not in profiles or not isinstance(profiles.get("Default"), dict):
                        profiles["Default"] = {}

                    default_profile = profiles["Default"]
                    default_profile["schedule_mode"] = "all_days"
                    default_profile["schedules"] = copy.deepcopy(group_data["schedules"])
                    group_data["profiles"] = profiles
                    group_data["active_profile"] = group_data.get("active_profile") or "Default"

                    _LOGGER.info(
                        f"Applied configured default schedule to group '{entity_group_name}' while enabling monitoring"
                    )

            _LOGGER.info(f"Set group '{entity_group_name}' ignored={ignored}, enabled={not ignored} for entity {entity_id}")
        else:
            # Entity is not in any group, create a single-entity group
            if "groups" not in self._data:
                self._data["groups"] = {}
            
            # Get friendly name for the group
            friendly_name = entity_id
            if state := self.hass.states.get(entity_id):
                friendly_name = state.attributes.get("friendly_name", entity_id)
            single_group_name = friendly_name
            
            if ignored:
                # Create with empty schedule and ignored=True
                self._data["groups"][single_group_name] = {
                    "entities": [entity_id],
                    "enabled": False,
                    "ignored": True,
                    "schedule_mode": "all_days",
                    "schedules": {"all_days": []},
                    "profiles": {
                        "Default": {
                            "schedule_mode": "all_days",
                            "schedules": {"all_days": []}
                        }
                    },
                    "active_profile": "Default",
                    "_is_single_entity_group": True
                }
                _LOGGER.info(f"Created single-entity group '{single_group_name}' for {entity_id} with ignored=True")
            else:
                # Create with default schedule and ignored=False
                default_schedule = self._get_default_schedule_template()
                self._data["groups"][single_group_name] = {
                    "entities": [entity_id],
                    "enabled": True,
                    "ignored": False,
                    "schedule_mode": "all_days",
                    "schedules": {"all_days": copy.deepcopy(default_schedule)},
                    "profiles": {
                        "Default": {
                            "schedule_mode": "all_days",
                            "schedules": {"all_days": copy.deepcopy(default_schedule)}
                        }
                    },
                    "active_profile": "Default",
                    "_is_single_entity_group": True
                }
                _LOGGER.info(f"Created single-entity group '{single_group_name}' for {entity_id} with default schedule")
        
        await self.async_save()
    
    async def async_is_ignored(self, entity_id: str) -> bool:
        """Check if an entity is marked as ignored."""
        # Check if entity is in any group
        for group_name, group_data in self._data.get("groups", {}).items():
            if entity_id in group_data.get("entities", []):
                return group_data.get("ignored", False)
        
        # Entity not found in any group
        return False

    async def async_set_enabled(self, entity_id: str, enabled: bool) -> None:
        """Enable or disable scheduling for an entity (via its single-entity group or multi-entity group)."""
        # Find which group this entity belongs to
        entity_group_name = None
        for group_name, group_data in self._data.get("groups", {}).items():
            if entity_id in group_data.get("entities", []):
                entity_group_name = group_name
                break
        
        # Update the group
        if entity_group_name:
            self._data["groups"][entity_group_name]["enabled"] = enabled
            await self.async_save()
            _LOGGER.info(f"Set group '{entity_group_name}' enabled={enabled} for entity {entity_id}")
        else:
            _LOGGER.warning(f"Entity {entity_id} not found in any group for async_set_enabled")
    
    async def _reload_sensor_platform(self) -> None:
        """Reload the sensor platform to update derivative sensors."""
        try:
            # Get the config entry
            entries = self.hass.config_entries.async_entries(DOMAIN)
            if entries:
                entry = entries[0]
                # Reload the sensor platform
                await self.hass.config_entries.async_reload(entry.entry_id)
                _LOGGER.debug("Reloaded sensor platform for derivative sensor update")
        except Exception as e:
            _LOGGER.debug(f"Could not reload sensor platform: {e}")
    
    async def async_cleanup_derivative_sensors(self, confirm_delete_all: bool = False) -> Dict[str, Any]:
        """Cleanup derivative sensors.
        
        - Delete sensors for entities that no longer exist
        - If auto-creation is disabled and confirm_delete_all=True, delete ALL derivative sensors
        
        Returns a dict with cleanup results.
        """
        settings = self._data.get("settings", {})
        auto_creation_enabled = settings.get("create_derivative_sensors", True)
        
        # Get all entity IDs from groups
        all_entity_ids = await self.async_get_all_entities()
        
        deleted_sensors = []
        errors = []
        
        # Get entity registry to find our sensors
        from homeassistant.helpers import entity_registry as er
        entity_registry = er.async_get(self.hass)
        
        # Find all climate_scheduler rate sensors
        climate_scheduler_sensors = [
            entry
            for entry in entity_registry.entities.values()
            if entry.platform == DOMAIN 
            and entry.domain == "sensor"
            and entry.unique_id and entry.unique_id.endswith("_rate")
        ]
        
        # If auto-creation is disabled and user confirmed, delete ALL
        if not auto_creation_enabled and confirm_delete_all:
            _LOGGER.debug(f"Auto-creation disabled and confirmed - deleting all {len(climate_scheduler_sensors)} derivative sensors")
            for entry in climate_scheduler_sensors:
                try:
                    entity_registry.async_remove(entry.entity_id)
                    deleted_sensors.append(entry.entity_id)
                    _LOGGER.debug(f"Deleted derivative sensor {entry.entity_id}")
                except Exception as e:
                    errors.append(f"{entry.entity_id}: {str(e)}")
                    _LOGGER.warning(f"Failed to delete {entry.entity_id}: {e}")
            
            return {
                "deleted_count": len(deleted_sensors),
                "deleted_sensors": deleted_sensors,
                "errors": errors,
                "message": f"Deleted all {len(deleted_sensors)} climate scheduler derivative sensors"
            }
        
        # If auto-creation is disabled but not confirmed, return warning
        if not auto_creation_enabled and not confirm_delete_all:
            return {
                "deleted_count": 0,
                "deleted_sensors": [],
                "errors": [],
                "message": f"Auto-creation is disabled. Found {len(climate_scheduler_sensors)} derivative sensors. Set confirm_delete_all=true to delete all.",
                "requires_confirmation": True
            }
        
        # Otherwise, only delete sensors for entities that no longer exist
        for entry in climate_scheduler_sensors:
            # Extract climate entity from unique_id: climate_scheduler_bedroom_rate -> climate.bedroom
            unique_id = entry.unique_id
            entity_name = unique_id.replace("climate_scheduler_", "").replace("_rate", "")
            climate_entity_id = f"climate.{entity_name}"
            
            if climate_entity_id not in all_entity_ids:
                # Entity no longer exists in storage, delete the sensor
                try:
                    entity_registry.async_remove(entry.entity_id)
                    deleted_sensors.append(entry.entity_id)
                    _LOGGER.debug(f"Deleted orphaned derivative sensor {entry.entity_id} (entity {climate_entity_id} no longer exists)")
                except Exception as e:
                    errors.append(f"{entry.entity_id}: {str(e)}")
                    _LOGGER.warning(f"Failed to delete {entry.entity_id}: {e}")
        
        return {
            "deleted_count": len(deleted_sensors),
            "deleted_sensors": deleted_sensors,
            "errors": errors,
            "message": f"Deleted {len(deleted_sensors)} orphaned derivative sensors"
        }

    async def async_cleanup_unmonitored_storage(self, delete: bool = False) -> Dict[str, Any]:
        """Cleanup stale storage references for unmonitored or missing entities.

        Removes:
        - Groups marked as ignored (unmonitored)
        - Groups with no valid climate entities
        - Invalid/missing entity references inside groups
        - Invalid profile structures and broken active profile pointers
        - Orphaned advance history entries
        """
        groups = self._data.get("groups", {})
        if not isinstance(groups, dict):
            groups = {}

        climate_entity_ids = set(self.hass.states.async_entity_ids("climate"))

        changed = False
        kept_groups: Dict[str, Any] = {}
        kept_entities: set[str] = set()

        removed_groups: list[Dict[str, Any]] = []
        removed_entity_refs: list[Dict[str, Any]] = []
        repaired_groups: list[str] = []
        removed_profiles: list[Dict[str, Any]] = []
        repaired_active_profiles: list[Dict[str, str]] = []
        removed_advance_history: list[str] = []

        for group_name, raw_group_data in groups.items():
            if not isinstance(raw_group_data, dict):
                removed_groups.append({"group": group_name, "reason": "invalid_group_data"})
                changed = True
                continue

            group_data = copy.deepcopy(raw_group_data)

            if group_data.get("ignored", False):
                removed_groups.append({"group": group_name, "reason": "unmonitored"})
                changed = True
                continue

            original_entities = group_data.get("entities", [])
            if not isinstance(original_entities, list):
                original_entities = []

            normalized_entities: list[str] = []
            seen_entities: set[str] = set()
            for entity_id in original_entities:
                if not isinstance(entity_id, str) or not entity_id:
                    removed_entity_refs.append({
                        "group": group_name,
                        "entity_id": entity_id,
                        "reason": "invalid_entity_reference",
                    })
                    changed = True
                    continue

                if entity_id in seen_entities:
                    removed_entity_refs.append({
                        "group": group_name,
                        "entity_id": entity_id,
                        "reason": "duplicate_entity_reference",
                    })
                    changed = True
                    continue

                seen_entities.add(entity_id)

                if entity_id not in climate_entity_ids:
                    removed_entity_refs.append({
                        "group": group_name,
                        "entity_id": entity_id,
                        "reason": "entity_not_found",
                    })
                    changed = True
                    continue

                normalized_entities.append(entity_id)

            if not normalized_entities:
                removed_groups.append({"group": group_name, "reason": "no_monitored_entities"})
                changed = True
                continue

            if normalized_entities != original_entities:
                group_data["entities"] = normalized_entities
                changed = True
                repaired_groups.append(group_name)

            profiles = group_data.get("profiles", {})
            if not isinstance(profiles, dict):
                profiles = {}
                changed = True

            valid_profiles: Dict[str, Any] = {}
            for profile_name, profile_data in profiles.items():
                if not isinstance(profile_data, dict):
                    removed_profiles.append({
                        "group": group_name,
                        "profile": profile_name,
                        "reason": "invalid_profile_data",
                    })
                    changed = True
                    continue

                profile_schedules = profile_data.get("schedules")
                if not isinstance(profile_schedules, dict):
                    removed_profiles.append({
                        "group": group_name,
                        "profile": profile_name,
                        "reason": "invalid_profile_schedules",
                    })
                    changed = True
                    continue

                valid_profiles[profile_name] = {
                    "schedule_mode": profile_data.get("schedule_mode", group_data.get("schedule_mode", "all_days")),
                    "schedules": copy.deepcopy(profile_schedules),
                }

                if profile_data.get("legacy") is True:
                    valid_profiles[profile_name]["legacy"] = True

            if not valid_profiles:
                valid_profiles["Default"] = {
                    "schedule_mode": group_data.get("schedule_mode", "all_days"),
                    "schedules": copy.deepcopy(group_data.get("schedules", {"all_days": []})),
                }
                changed = True

            group_data["profiles"] = valid_profiles

            active_profile = group_data.get("active_profile")
            if active_profile not in valid_profiles:
                new_active_profile = "Default" if "Default" in valid_profiles else next(iter(valid_profiles))
                repaired_active_profiles.append({
                    "group": group_name,
                    "old": str(active_profile),
                    "new": new_active_profile,
                })
                group_data["active_profile"] = new_active_profile
                changed = True

            active_profile_data = group_data["profiles"][group_data["active_profile"]]
            group_data["schedule_mode"] = active_profile_data.get("schedule_mode", "all_days")
            group_data["schedules"] = copy.deepcopy(active_profile_data.get("schedules", {"all_days": []}))

            should_be_single = len(normalized_entities) == 1
            if group_data.get("_is_single_entity_group", False) != should_be_single:
                group_data["_is_single_entity_group"] = should_be_single
                changed = True
                if group_name not in repaired_groups:
                    repaired_groups.append(group_name)

            kept_groups[group_name] = group_data
            kept_entities.update(normalized_entities)

        current_groups = self._data.get("groups", {})
        if current_groups != kept_groups:
            changed = True

        advance_history_raw = self._data.get("advance_history", {})
        advance_history: Dict[str, Any] = copy.deepcopy(advance_history_raw) if isinstance(advance_history_raw, dict) else {}
        if not isinstance(advance_history_raw, dict):
            changed = True

        for entity_id in list(advance_history.keys()):
            if entity_id not in kept_entities:
                removed_advance_history.append(entity_id)
                del advance_history[entity_id]
                changed = True

        if "entities" in self._data and self._data.get("entities"):
            changed = True

        if not delete:
            message = (
                f"Preview: would remove {len(removed_groups)} groups, "
                f"{len(removed_entity_refs)} stale entity references, "
                f"{len(removed_profiles)} invalid profiles, and "
                f"{len(removed_advance_history)} orphaned advance history entries."
            )

            return {
                "delete": False,
                "would_change": changed,
                "would_groups_remaining": len(kept_groups),
                "would_remove_groups": removed_groups,
                "would_remove_entity_references": removed_entity_refs,
                "would_remove_profiles": removed_profiles,
                "would_repair_groups": repaired_groups,
                "would_repair_active_profiles": repaired_active_profiles,
                "would_remove_advance_history": removed_advance_history,
                "message": message,
            }

        self._data["groups"] = kept_groups
        self._data["advance_history"] = advance_history
        if "entities" in self._data:
            del self._data["entities"]

        if changed:
            await self.async_save()

        message = (
            f"Removed {len(removed_groups)} groups, "
            f"{len(removed_entity_refs)} stale entity references, "
            f"{len(removed_profiles)} invalid profiles, and "
            f"{len(removed_advance_history)} orphaned advance history entries."
        )

        return {
            "delete": True,
            "changed": changed,
            "groups_remaining": len(kept_groups),
            "removed_groups": removed_groups,
            "removed_entity_references": removed_entity_refs,
            "removed_profiles": removed_profiles,
            "repaired_groups": repaired_groups,
            "repaired_active_profiles": repaired_active_profiles,
            "removed_advance_history": removed_advance_history,
            "message": message,
        }

    async def async_is_enabled(self, entity_id: str) -> bool:
        """Check if scheduling is enabled for an entity."""
        # Check if entity is in any group
        for group_name, group_data in self._data.get("groups", {}).items():
            if entity_id in group_data.get("entities", []):
                return group_data.get("enabled", True)
        
        # Entity not found in any group
        return False

    def interpolate_temperature(self, nodes: List[Dict[str, Any]], current_time: time) -> float:
        """Calculate temperature at a given time using step function (hold until next node)."""
        if not nodes:
            return 18.0  # Default fallback
        
        # Sort nodes by time
        sorted_nodes = sorted(nodes, key=lambda n: self._time_to_minutes(n["time"]))
        
        # Convert current time to minutes since midnight
        current_minutes = current_time.hour * 60 + current_time.minute
        
        # Find the active node (most recent node before or at current time)
        active_node = None
        
        for node in sorted_nodes:
            node_minutes = self._time_to_minutes(node["time"])
            if node_minutes <= current_minutes:
                active_node = node
            else:
                break
        
        # If no node found before current time, use last node from previous day (wrap around)
        if active_node is None:
            active_node = sorted_nodes[-1]
        
        return active_node["temp"]

    def get_active_node(self, nodes: List[Dict[str, Any]], current_time: time) -> Optional[Dict[str, Any]]:
        """Get the active node at a given time."""
        if not nodes:
            return None
        
        # Sort nodes by time
        sorted_nodes = sorted(nodes, key=lambda n: self._time_to_minutes(n["time"]))
        
        # Convert current time to minutes since midnight
        current_minutes = current_time.hour * 60 + current_time.minute
        
        # Find the active node (most recent node before or at current time)
        active_node = None
        
        for node in sorted_nodes:
            node_minutes = self._time_to_minutes(node["time"])
            if node_minutes <= current_minutes:
                active_node = node
            else:
                break
        
        # If no node found before current time, use last node from previous day (wrap around)
        if active_node is None:
            active_node = sorted_nodes[-1]
        
        return active_node
    
    async def get_active_node_for_group(self, group_name: str, current_time: Optional[time] = None, current_day: Optional[str] = None) -> Optional[Dict[str, Any]]:
        """Get the active node for a group at a given time, handling cross-day transitions in individual mode.
        
        Args:
            group_name: Name of the group
            current_time: Time to check (defaults to now)
            current_day: Day abbreviation (mon, tue, etc.) (defaults to today)
            
        Returns:
            The active node at the specified time, or None if no schedule exists
        """
        if current_time is None:
            current_time = datetime.now().time()
        if current_day is None:
            current_day = datetime.now().strftime('%a').lower()
        
        # Get group schedule for current day
        group_schedule = await self.async_get_group_schedule(group_name, current_day)
        if not group_schedule or not group_schedule.get("nodes"):
            return None
        
        schedule_mode = group_schedule.get("schedule_mode", "all_days")
        nodes = group_schedule["nodes"]
        
        # In individual or 5/2 mode, check if we need previous day's schedule
        if schedule_mode in ["individual", "5/2"] and nodes:
            sorted_nodes = sorted(nodes, key=lambda n: self._time_to_minutes(n["time"]))
            current_minutes = current_time.hour * 60 + current_time.minute
            first_node_minutes = self._time_to_minutes(sorted_nodes[0]["time"])
            
            if current_minutes < first_node_minutes:
                # We're before the first node of today, get previous day/period's last node
                days_of_week = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
                current_day_index = days_of_week.index(current_day)
                prev_day = days_of_week[(current_day_index - 1) % 7]
                
                prev_day_schedule = await self.async_get_group_schedule(group_name, prev_day)
                if prev_day_schedule and prev_day_schedule.get("nodes"):
                    prev_nodes = prev_day_schedule["nodes"]
                    sorted_prev_nodes = sorted(prev_nodes, key=lambda n: self._time_to_minutes(n["time"]))
                    return sorted_prev_nodes[-1]
        
        # Normal case: get active node from current day's schedule
        return self.get_active_node(nodes, current_time)
    
    def get_next_node(self, nodes: List[Dict[str, Any]], current_time: time) -> Optional[Dict[str, Any]]:
        """Get the next scheduled node after the current time."""
        if not nodes:
            return None
        
        # Sort nodes by time
        sorted_nodes = sorted(nodes, key=lambda n: self._time_to_minutes(n["time"]))
        
        # Convert current time to minutes since midnight
        current_minutes = current_time.hour * 60 + current_time.minute
        
        # Find the next node (first node after current time)
        for node in sorted_nodes:
            node_minutes = self._time_to_minutes(node["time"])
            if node_minutes > current_minutes:
                return node
        
        # If no node found after current time, wrap around to first node (next day)
        return sorted_nodes[0]

    @staticmethod
    def _time_to_minutes(time_str: str) -> int:
        """Convert HH:MM string to minutes since midnight."""
        parts = time_str.split(":")
        hours = int(parts[0])
        minutes = int(parts[1])
        return hours * 60 + minutes

    # Group Management Methods

    async def async_create_group(self, group_name: str) -> None:
        """Create a new group."""
        if "groups" not in self._data:
            self._data["groups"] = {}
        
        if group_name in self._data["groups"]:
            raise ValueError(f"Group '{group_name}' already exists")
        
        self._data["groups"][group_name] = {
            "entities": [],
            "enabled": True,
            "ignored": False,
            "schedule_mode": "all_days",
            "schedules": {
                "all_days": [{"time": "00:00", "temp": 18}]
            },
            "profiles": {
                "Default": {
                    "schedule_mode": "all_days",
                    "schedules": {
                        "all_days": [{"time": "00:00", "temp": 18}]
                    }
                }
            },
            "active_profile": "Default"
        }
        await self.async_save()
        _LOGGER.info(f"Created group '{group_name}'")

    async def async_delete_group(self, group_name: str) -> None:
        """Delete a group."""
        if group_name in self._data.get("groups", {}):
            del self._data["groups"][group_name]
            await self.async_save()
            _LOGGER.info(f"Deleted group '{group_name}'")
    
    async def async_rename_group(self, old_name: str, new_name: str) -> None:
        """Rename a group."""
        if old_name not in self._data.get("groups", {}):
            raise ValueError(f"Group '{old_name}' does not exist")
        
        if new_name in self._data.get("groups", {}):
            raise ValueError(f"Group '{new_name}' already exists")
        
        # Rename the group
        self._data["groups"][new_name] = self._data["groups"].pop(old_name)
        await self.async_save()
        _LOGGER.info(f"Renamed group from '{old_name}' to '{new_name}'")

    async def async_add_entity_to_group(self, group_name: str, entity_id: str) -> None:
        """Add an entity to a group.
        
        If the entity is unmonitored (not in any group), it will be added to the target group.
        If the entity is in a single-entity group, that group will be deleted and the entity moved.
        """
        if group_name not in self._data.get("groups", {}):
            raise ValueError(f"Group '{group_name}' does not exist")
        
        group_data = self._data["groups"][group_name]
        
        # Check if entity is currently in a different group
        old_single_entity_group = None
        entity_found_in_group = False
        for existing_group_name, existing_group_data in self._data.get("groups", {}).items():
            if existing_group_name != group_name and entity_id in existing_group_data.get("entities", []):
                # Found entity in a different group
                entity_found_in_group = True
                if existing_group_data.get("_is_single_entity_group") and len(existing_group_data.get("entities", [])) == 1:
                    # It's a single-entity group - mark for deletion
                    old_single_entity_group = existing_group_name
                    _LOGGER.info(f"Entity {entity_id} is in single-entity group '{existing_group_name}' which will be deleted")
                # Remove entity from the old group
                existing_group_data["entities"].remove(entity_id)
                break
        
        # Log if entity wasn't in any group (unmonitored entity being added)
        if not entity_found_in_group:
            _LOGGER.info(f"Adding unmonitored entity {entity_id} to group '{group_name}'")
        
        if entity_id not in group_data["entities"]:
            group_data["entities"].append(entity_id)
            entity_count = len(group_data["entities"])
            
            # Normalize _is_single_entity_group flag based on actual entity count
            if entity_count == 1:
                # Group now has exactly 1 entity - mark as single-entity group
                group_data["_is_single_entity_group"] = True
                _LOGGER.info(f"Group '{group_name}' now has 1 entity - marked as single-entity group")
            elif entity_count >= 2:
                # Group now has 2+ entities - ensure it's marked as multi-entity group
                if group_data.get("_is_single_entity_group"):
                    group_data["_is_single_entity_group"] = False
                    _LOGGER.info(f"Group '{group_name}' now has {entity_count} entities - converted to multi-entity group")
            
            # Delete the old single-entity group if it existed
            if old_single_entity_group:
                del self._data["groups"][old_single_entity_group]
                _LOGGER.info(f"Deleted single-entity group '{old_single_entity_group}'")
            
            await self.async_save()
            _LOGGER.info(f"Added {entity_id} to group '{group_name}'")

    async def async_remove_entity_from_group(self, group_name: str, entity_id: str) -> None:
        """Remove an entity from a group and create a new single-entity group for it."""
        if group_name in self._data.get("groups", {}):
            entities = self._data["groups"][group_name]["entities"]
            if entity_id in entities:
                # Get the current group data before removing the entity
                group_data = self._data["groups"][group_name]
                
                entities.remove(entity_id)
                
                # If only 1 entity remains, convert the group to a single-entity group
                if len(entities) == 1:
                    group_data["_is_single_entity_group"] = True
                    _LOGGER.info(f"Group '{group_name}' now has only 1 entity - converted to single-entity group")
                
                # Create a new single-entity group for the removed entity using friendly name
                friendly_name = entity_id
                if state := self.hass.states.get(entity_id):
                    friendly_name = state.attributes.get("friendly_name", entity_id)
                single_group_name = friendly_name
                
                if "groups" not in self._data:
                    self._data["groups"] = {}
                
                # Create the new single-entity group with current data from the group
                self._data["groups"][single_group_name] = {
                    "entities": [entity_id],
                    "enabled": group_data.get("enabled", True),
                    "ignored": group_data.get("ignored", False),
                    "schedule_mode": group_data.get("schedule_mode", "all_days"),
                    "schedules": copy.deepcopy(group_data.get("schedules", {"all_days": []})),
                    "profiles": copy.deepcopy(group_data.get("profiles", {
                        "Default": {
                            "schedule_mode": "all_days",
                            "schedules": {"all_days": []}
                        }
                    })),
                    "active_profile": group_data.get("active_profile", "Default"),
                    "_is_single_entity_group": True
                }
                
                await self.async_save()
                _LOGGER.info(f"Removed {entity_id} from group '{group_name}' and created single-entity group '{single_group_name}'")

    async def async_get_groups(self) -> Dict[str, Any]:
        """Get all groups."""
        self._sync_group_profile_views()
        groups = self._data.get("groups", {})
        projected: Dict[str, Any] = {}
        for group_name, group_data in groups.items():
            if isinstance(group_data, dict):
                projected[group_name] = self._project_group_runtime_view(group_data)
            else:
                projected[group_name] = group_data
        return projected

    async def async_get_group(self, group_name: str) -> Optional[Dict[str, Any]]:
        """Get a specific group."""
        self._sync_group_profile_views()
        group_data = self._data.get("groups", {}).get(group_name)
        if not isinstance(group_data, dict):
            return group_data
        return self._project_group_runtime_view(group_data)

    def _project_group_runtime_view(self, group_data: Dict[str, Any]) -> Dict[str, Any]:
        """Return a runtime view exposing global profiles while preserving persisted legacy profiles."""
        view = copy.deepcopy(group_data)
        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})
        view["profiles"] = copy.deepcopy(global_profiles)

        active_profile = self._resolve_group_active_global_profile(group_data, global_profiles)
        if active_profile:
            view["active_profile"] = active_profile
            view["active_profile_global"] = active_profile

        return view

    async def async_get_entity_group(self, entity_id: str) -> Optional[str]:
        """Get the group name that an entity belongs to."""
        for group_name, group_data in self._data.get("groups", {}).items():
            if entity_id in group_data.get("entities", []):
                return group_name
        return None

    async def async_set_group_schedule(self, group_name: str, nodes: List[Dict[str, Any]], day: Optional[str] = None, schedule_mode: Optional[str] = None, profile_name: Optional[str] = None) -> None:
        """Set schedule for a group (applies to all entities in the group).
        
        If profile_name is provided, saves to that specific profile without changing the active profile.
        Otherwise saves to the currently active profile.
        """
        if group_name not in self._data.get("groups", {}):
            raise ValueError(f"Group '{group_name}' does not exist")
        
        group_data = self._data["groups"][group_name]

        def apply_nodes_to_schedules(
            schedules: Dict[str, Any],
            mode: str,
            schedule_nodes: List[Dict[str, Any]],
            schedule_day: Optional[str],
        ) -> Dict[str, Any]:
            """Apply incoming nodes to a schedule dict using mode/day mapping semantics."""
            updated = copy.deepcopy(schedules) if isinstance(schedules, dict) else {}

            if schedule_day is None:
                if mode == "all_days":
                    updated["all_days"] = schedule_nodes
                elif mode == "5/2":
                    updated["weekday"] = schedule_nodes
                else:
                    updated["mon"] = schedule_nodes
                return updated

            if mode == "5/2":
                if schedule_day == "weekday":
                    updated["weekday"] = schedule_nodes
                elif schedule_day == "weekend":
                    updated["weekend"] = schedule_nodes
                elif schedule_day in ["mon", "tue", "wed", "thu", "fri"]:
                    updated["weekday"] = schedule_nodes
                else:
                    updated["weekend"] = schedule_nodes
            else:
                updated[schedule_day] = schedule_nodes

            return updated

        # Determine which profile to save to (global profile namespace)
        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})

        resolved_active_profile = self._resolve_group_active_global_profile(group_data, global_profiles)
        target_profile = profile_name if profile_name else resolved_active_profile

        # Verify profile exists if explicitly specified
        if profile_name and target_profile not in global_profiles:
            raise ValueError(f"Profile '{profile_name}' does not exist")

        # Create profile if it doesn't exist (only for active profile flow)
        if target_profile not in global_profiles:
            global_profiles[target_profile] = {
                "schedule_mode": group_data.get("schedule_mode", "all_days"),
                "schedules": {}
            }

        is_explicit_non_active_profile_save = bool(profile_name and profile_name != resolved_active_profile)

        if is_explicit_non_active_profile_save:
            target_mode = schedule_mode if schedule_mode is not None else global_profiles[target_profile].get("schedule_mode", "all_days")
            target_schedules = apply_nodes_to_schedules(
                global_profiles[target_profile].get("schedules", {}),
                target_mode,
                nodes,
                day,
            )

            global_profiles[target_profile]["schedule_mode"] = target_mode
            global_profiles[target_profile]["schedules"] = copy.deepcopy(target_schedules)
        else:
            # Active-profile save path updates group runtime state and mirrors to active global profile
            if schedule_mode is not None:
                group_data["schedule_mode"] = schedule_mode

            current_mode = group_data.get("schedule_mode", "all_days")
            group_data["schedules"] = apply_nodes_to_schedules(
                group_data.get("schedules", {}),
                current_mode,
                nodes,
                day,
            )

            global_profiles[target_profile]["schedule_mode"] = current_mode
            global_profiles[target_profile]["schedules"] = copy.deepcopy(group_data["schedules"])

        self._data["profiles"] = global_profiles

        # Keep group runtime schedule aligned with active profile for non-active profile saves
        if is_explicit_non_active_profile_save:
            active_profile = resolved_active_profile
            if active_profile in global_profiles:
                group_data["schedule_mode"] = global_profiles[active_profile].get("schedule_mode", "all_days")
                group_data["schedules"] = copy.deepcopy(global_profiles[active_profile].get("schedules", {}))

            current_mode = global_profiles[target_profile].get("schedule_mode", "all_days")
        else:
            current_mode = group_data.get("schedule_mode", "all_days")
        
        _LOGGER.info(f"Saved group schedule to profile '{target_profile}' for group '{group_name}' - day: {day}, mode: {current_mode}, nodes: {len(nodes)}")
        _LOGGER.debug(f"Profile schedules after save: {global_profiles[target_profile]['schedules'].keys()}")
        
        await self.async_save()
        _LOGGER.info(f"Set schedule for group '{group_name}' with {len(nodes)} nodes (day: {day}, mode: {schedule_mode})")
    
    async def async_get_group_schedule(self, group_name: str, day: Optional[str] = None) -> Optional[Dict[str, Any]]:
        """Get schedule for a group (same logic as entity schedule retrieval)."""
        if group_name not in self._data.get("groups", {}):
            return None
        
        group_data = self._data["groups"][group_name]
        schedule_mode = group_data.get("schedule_mode", "all_days")
        schedules = group_data.get("schedules", {})
        
        # Same day resolution logic as entity schedules
        if schedule_mode == "all_days":
            nodes = schedules.get("all_days", [])
        elif schedule_mode == "5/2":
            if day in ["mon", "tue", "wed", "thu", "fri"]:
                nodes = schedules.get("weekday", [])
            else:  # sat, sun
                nodes = schedules.get("weekend", [])
        else:  # individual
            nodes = schedules.get(day, [])
        
        return {
            "nodes": nodes,
            "enabled": group_data.get("enabled", True),
            "schedule_mode": schedule_mode,
            "schedules": schedules
        }
    
    async def async_enable_group(self, group_name: str) -> None:
        """Enable a group schedule."""
        if group_name not in self._data.get("groups", {}):
            raise ValueError(f"Group '{group_name}' does not exist")
        
        self._data["groups"][group_name]["enabled"] = True
        await self.async_save()
        _LOGGER.info(f"Enabled group '{group_name}'")
    
    async def async_disable_group(self, group_name: str) -> None:
        """Disable a group schedule."""
        if group_name not in self._data.get("groups", {}):
            raise ValueError(f"Group '{group_name}' does not exist")
        
        self._data["groups"][group_name]["enabled"] = False
        await self.async_save()
        _LOGGER.info(f"Disabled group '{group_name}'")
    
    async def async_enable_schedule(self, schedule_id: str) -> None:
        """Enable a schedule by group name or entity_id."""
        # Check if it's a group name
        if schedule_id in self._data.get("groups", {}):
            await self.async_enable_group(schedule_id)
        else:
            # Treat as entity_id - find its group
            group_name = await self.async_get_entity_group(schedule_id)
            if group_name:
                await self.async_enable_group(group_name)
            else:
                raise ValueError(f"Schedule '{schedule_id}' not found")
    
    async def async_disable_schedule(self, schedule_id: str) -> None:
        """Disable a schedule by group name or entity_id."""
        # Check if it's a group name
        if schedule_id in self._data.get("groups", {}):
            await self.async_disable_group(schedule_id)
        else:
            # Treat as entity_id - find its group
            group_name = await self.async_get_entity_group(schedule_id)
            if group_name:
                await self.async_disable_group(group_name)
            else:
                raise ValueError(f"Schedule '{schedule_id}' not found")
    
    # Profile Management Methods
    
    async def async_create_profile(self, target_id: str, profile_name: str) -> None:
        """Create a new global schedule profile."""
        if target_id not in self._data.get("groups", {}):
            raise ValueError(f"Group '{target_id}' does not exist")

        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})

        if profile_name in global_profiles:
            raise ValueError(f"Profile '{profile_name}' already exists")

        # Seed from current target group's active schedule for convenience
        target_group = self._data["groups"][target_id]
        global_profiles[profile_name] = {
            "schedule_mode": target_group.get("schedule_mode", "all_days"),
            "schedules": copy.deepcopy(target_group.get("schedules", {"all_days": []}))
        }
        self._data["profiles"] = global_profiles

        await self.async_save()
        _LOGGER.info(f"Created global profile '{profile_name}'")
    
    async def async_delete_profile(self, target_id: str, profile_name: str) -> None:
        """Delete a global schedule profile."""
        if target_id not in self._data.get("groups", {}):
            raise ValueError(f"Group '{target_id}' does not exist")

        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})

        if profile_name not in global_profiles:
            raise ValueError(f"Profile '{profile_name}' does not exist")

        if len(global_profiles) <= 1:
            raise ValueError(f"Cannot delete the last profile")

        for group_name, group_data in self._data.get("groups", {}).items():
            if group_data.get("active_profile") == profile_name:
                raise ValueError(f"Cannot delete profile '{profile_name}' because it is active for group '{group_name}'")

        del global_profiles[profile_name]
        self._data["profiles"] = global_profiles

        await self.async_save()
        _LOGGER.info(f"Deleted global profile '{profile_name}'")
    
    async def async_rename_profile(self, target_id: str, old_name: str, new_name: str) -> None:
        """Rename a global schedule profile."""
        if target_id not in self._data.get("groups", {}):
            raise ValueError(f"Group '{target_id}' does not exist")

        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})

        if old_name not in global_profiles:
            raise ValueError(f"Profile '{old_name}' does not exist")

        if new_name in global_profiles:
            raise ValueError(f"Profile '{new_name}' already exists")

        global_profiles[new_name] = global_profiles.pop(old_name)
        self._data["profiles"] = global_profiles

        for group_data in self._data.get("groups", {}).values():
            if group_data.get("active_profile") == old_name:
                group_data["active_profile"] = new_name

        await self.async_save()
        _LOGGER.info(f"Renamed global profile from '{old_name}' to '{new_name}'")
    
    async def async_set_active_profile(self, target_id: str, profile_name: str) -> None:
        """Set the active profile for a group."""

        if target_id not in self._data.get("groups", {}):
            raise ValueError(f"Group '{target_id}' does not exist")

        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})

        if profile_name not in global_profiles:
            raise ValueError(f"Profile '{profile_name}' does not exist")

        target_data = self._data["groups"][target_id]

        # Set the active global profile (runtime path)
        target_data["active_profile_global"] = profile_name

        # Keep legacy active profile pointer for downgrade compatibility
        legacy_profiles = target_data.get("profiles", {})
        legacy_active = target_data.get("active_profile")
        legacy_candidate = None
        if isinstance(legacy_profiles, dict):
            if profile_name in legacy_profiles:
                legacy_candidate = profile_name
            else:
                prefix = f"{target_id} - "
                if profile_name.startswith(prefix):
                    original_name = profile_name[len(prefix):]

                    # Preferred mapping: original legacy profile key with explicit metadata
                    original_profile = legacy_profiles.get(original_name)
                    if isinstance(original_profile, dict) and original_profile.get("legacy") is True:
                        legacy_candidate = original_name
                    else:
                        # Backward compatibility for pre-metadata snapshots using name suffix tagging
                        tagged_name = f"{original_name} [legacy]"
                        tagged_profile = legacy_profiles.get(tagged_name)
                        if isinstance(tagged_profile, dict):
                            legacy_candidate = tagged_name

        if legacy_candidate:
            target_data["active_profile"] = legacy_candidate
            target_data["active_profile_legacy"] = legacy_candidate
        elif legacy_active in legacy_profiles:
            target_data["active_profile"] = legacy_active
            target_data["active_profile_legacy"] = legacy_active
        elif isinstance(legacy_profiles, dict) and legacy_profiles:
            fallback_legacy = next(iter(legacy_profiles))
            target_data["active_profile"] = fallback_legacy
            target_data["active_profile_legacy"] = fallback_legacy

        # Load the profile's schedule into the main schedule fields
        profile_data = global_profiles[profile_name]
        target_data["schedule_mode"] = profile_data.get("schedule_mode", "all_days")
        target_data["schedules"] = copy.deepcopy(profile_data.get("schedules", {}))
        
        _LOGGER.info(f"Switched to profile '{profile_name}' for group '{target_id}'")
        _LOGGER.debug(f"Loaded schedule mode: {target_data['schedule_mode']}, schedule keys: {target_data['schedules'].keys()}")
        
        await self.async_save()
        _LOGGER.info(f"Set active profile to '{profile_name}' for group '{target_id}'")
    
    async def async_get_profiles(self, target_id: str) -> Dict[str, Any]:
        """Get all global profiles."""
        if target_id not in self._data.get("groups", {}):
            return {}

        self._ensure_global_profiles_initialized()
        return self._data.get("profiles", {})
    
    async def async_get_active_profile_name(self, target_id: str) -> Optional[str]:
        """Get the name of the active profile for a group."""
        target_key = "groups"
        
        if target_id not in self._data.get(target_key, {}):
            return None
        
        target_data = self._data[target_key][target_id]
        self._ensure_global_profiles_initialized()
        global_profiles = self._data.get("profiles", {})
        return self._resolve_group_active_global_profile(target_data, global_profiles)

    async def async_get_global_profiles(self) -> Dict[str, Any]:
        """Get the global profile dictionary."""
        self._ensure_global_profiles_initialized()
        return self._data.get("profiles", {})