"""WebSocket handlers for adaptive scheduling and analysis."""

from __future__ import annotations

from typing import Any

import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from ..const import (
    ADAPTIVE_MIN_INTERVAL_CAP_DAYS,
    CONF_TASKS,
    DOMAIN,
    MAX_ENTITY_ID_LENGTH,
    MAX_ID_LENGTH,
    MAX_META_LENGTH,
)
from ..helpers.permissions import require_write
from ..helpers.task_fields import INTERVAL_DAYS_RANGE
from . import _get_merged_tasks, _get_runtime_data, _load_object_entry


async def _persist_adaptive_config(
    hass: HomeAssistant,
    entry: ConfigEntry,
    task_id: str,
    adaptive_config: dict[str, Any],
) -> None:
    """Write a task's adaptive_config (store, or legacy entry data) and refresh."""
    rd = _get_runtime_data(hass, entry.entry_id)
    store = getattr(rd, "store", None) if rd else None
    if store is not None:
        store.set_adaptive_config(task_id, adaptive_config)
        store.async_delay_save()
    else:
        # Legacy: write to ConfigEntry.data
        static_tasks = dict(entry.data.get(CONF_TASKS, {}))
        task = dict(static_tasks[task_id])
        task["adaptive_config"] = adaptive_config
        static_tasks[task_id] = task
        new_data = dict(entry.data)
        new_data[CONF_TASKS] = static_tasks
        hass.config_entries.async_update_entry(entry, data=new_data)

    if rd and rd.coordinator:
        await rd.coordinator.async_refresh_now()


@websocket_api.websocket_command(
    {
        vol.Required("type"): f"{DOMAIN}/task/analyze_interval",
        vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Required("task_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
    }
)
@websocket_api.async_response
async def ws_analyze_interval(
    hass: HomeAssistant,
    connection: websocket_api.ActiveConnection,
    msg: dict[str, Any],
) -> None:
    """Return full interval analysis for a task (on-demand)."""
    from ..helpers.interval_analyzer import IntervalAnalyzer

    entry = _load_object_entry(hass, connection, msg)
    if entry is None:
        return

    tasks_data = _get_merged_tasks(entry)
    task_id = msg["task_id"]
    if task_id not in tasks_data:
        connection.send_error(msg["id"], "not_found", "Task not found")
        return

    task_data = tasks_data[task_id]
    adaptive_config = dict(task_data.get("adaptive_config", {}))

    # Inject hemisphere and current month for seasonal awareness
    from homeassistant.util import dt as dt_util

    adaptive_config["hemisphere"] = "south" if (hass.config.latitude or 0) < 0 else "north"
    adaptive_config["_current_month"] = dt_util.now().month

    analyzer = IntervalAnalyzer()
    analysis = analyzer.analyze(task_data, adaptive_config)

    connection.send_result(
        msg["id"],
        {
            "current_interval": analysis.current_interval,
            "average_actual_interval": analysis.average_actual_interval,
            "interval_std_dev": analysis.interval_std_dev,
            "ewa_prediction": analysis.ewa_prediction,
            "weibull_prediction": analysis.weibull_prediction,
            "weibull_beta": analysis.weibull_beta,
            "weibull_eta": analysis.weibull_eta,
            "recommended_interval": analysis.recommended_interval,
            "confidence": analysis.confidence,
            "feedback_count": analysis.feedback_count,
            "data_points": analysis.data_points,
            "recommendation_reason": analysis.recommendation_reason,
            "seasonal_factor": analysis.seasonal_factor,
            "seasonal_factors": analysis.seasonal_factors,
            "seasonal_reason": analysis.seasonal_adjustment_reason,
            "weibull_r_squared": analysis.weibull_r_squared,
            "confidence_interval_low": analysis.confidence_interval_low,
            "confidence_interval_high": analysis.confidence_interval_high,
        },
    )


@websocket_api.websocket_command(
    {
        vol.Required("type"): f"{DOMAIN}/task/apply_suggestion",
        vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Required("task_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Required("interval"): vol.All(int, vol.Range(min=INTERVAL_DAYS_RANGE[0], max=INTERVAL_DAYS_RANGE[1])),
    }
)
@require_write
@websocket_api.async_response
async def ws_apply_suggestion(
    hass: HomeAssistant,
    connection: websocket_api.ActiveConnection,
    msg: dict[str, Any],
) -> None:
    """Apply a suggested interval to a task."""
    rd = _get_runtime_data(hass, msg["entry_id"])
    if rd is None or rd.coordinator is None:
        connection.send_error(msg["id"], "not_found", "Coordinator not found")
        return

    # Reject an unknown task_id instead of silently no-opping and returning
    # success (the apply call is a no-op for a missing task).
    if msg["task_id"] not in rd.coordinator.entry.data.get(CONF_TASKS, {}):
        connection.send_error(msg["id"], "not_found", "Task not found")
        return

    await rd.coordinator.async_apply_suggested_interval(
        task_id=msg["task_id"],
        interval=msg["interval"],
    )
    connection.send_result(msg["id"], {"success": True})


@websocket_api.websocket_command(
    {
        vol.Required("type"): f"{DOMAIN}/task/seasonal_overrides",
        vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Required("task_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        # At most 12 month keys survive validation anyway — cap the input so a
        # giant dict isn't iterated/coerced first (parity with every other list).
        vol.Required("overrides"): vol.All(dict, vol.Length(max=12)),
    }
)
@require_write
@websocket_api.async_response
async def ws_seasonal_overrides(
    hass: HomeAssistant,
    connection: websocket_api.ActiveConnection,
    msg: dict[str, Any],
) -> None:
    """Set manual seasonal overrides for a task.

    Overrides is a dict of {month_num: factor}, e.g. {7: 0.5, 1: 2.0}.
    Keys must be 1-12, values must be 0.1-5.0.
    Pass empty dict {} to clear all overrides.
    """
    entry = _load_object_entry(hass, connection, msg)
    if entry is None:
        return

    task_id = msg["task_id"]
    tasks_data = _get_merged_tasks(entry)
    if task_id not in tasks_data:
        connection.send_error(msg["id"], "not_found", "Task not found")
        return

    # Validate overrides
    overrides = msg["overrides"]
    validated: dict[int, float] = {}
    for key, value in overrides.items():
        try:
            month = int(key)
            factor = float(value)
        except (ValueError, TypeError):
            connection.send_error(msg["id"], "invalid_input", f"Invalid override: key={key}, value={value}")
            return
        if month < 1 or month > 12:
            connection.send_error(msg["id"], "invalid_input", f"Month must be 1-12, got {month}")
            return
        if factor < 0.1 or factor > 5.0:
            connection.send_error(msg["id"], "invalid_input", f"Factor must be 0.1-5.0, got {factor}")
            return
        validated[month] = round(factor, 2)

    # Persist overrides in adaptive_config
    adaptive_config = dict(tasks_data[task_id].get("adaptive_config", {}))
    if validated:
        adaptive_config["seasonal_overrides"] = validated
    else:
        adaptive_config.pop("seasonal_overrides", None)

    await _persist_adaptive_config(hass, entry, task_id, adaptive_config)

    connection.send_result(msg["id"], {"success": True, "overrides": validated})


@websocket_api.websocket_command(
    {
        vol.Required("type"): f"{DOMAIN}/task/set_environmental_entity",
        vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Required("task_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Optional("environmental_entity"): vol.Any(vol.All(str, vol.Length(max=MAX_ENTITY_ID_LENGTH)), None),
        vol.Optional("environmental_attribute"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
    }
)
@require_write
@websocket_api.async_response
async def ws_set_environmental_entity(
    hass: HomeAssistant,
    connection: websocket_api.ActiveConnection,
    msg: dict[str, Any],
) -> None:
    """Set or clear the environmental entity for sensor-driven predictions.

    When set, the environmental sensor (e.g. outdoor temperature) is
    correlated with maintenance intervals to produce an adjustment factor.
    Pass environmental_entity=null to clear the binding.
    """
    entry = _load_object_entry(hass, connection, msg)
    if entry is None:
        return

    task_id = msg["task_id"]
    tasks_data = _get_merged_tasks(entry)
    if task_id not in tasks_data:
        connection.send_error(msg["id"], "not_found", "Task not found")
        return

    adaptive_config = dict(tasks_data[task_id].get("adaptive_config", {}))

    env_entity = msg.get("environmental_entity")
    env_attribute = msg.get("environmental_attribute")

    if env_entity:
        adaptive_config["environmental_entity"] = env_entity
        if env_attribute:
            adaptive_config["environmental_attribute"] = env_attribute
        else:
            adaptive_config.pop("environmental_attribute", None)
    else:
        # Clear environmental binding
        adaptive_config.pop("environmental_entity", None)
        adaptive_config.pop("environmental_attribute", None)

    await _persist_adaptive_config(hass, entry, task_id, adaptive_config)

    connection.send_result(
        msg["id"],
        {
            "success": True,
            "environmental_entity": env_entity,
            "environmental_attribute": env_attribute,
        },
    )


@websocket_api.websocket_command(
    {
        vol.Required("type"): f"{DOMAIN}/task/set_adaptive",
        vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Required("task_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
        vol.Required("enabled"): bool,
        vol.Optional("ewa_alpha"): vol.All(vol.Coerce(float), vol.Range(min=0.1, max=0.9)),
        vol.Optional("min_interval_days"): vol.All(int, vol.Range(min=1, max=ADAPTIVE_MIN_INTERVAL_CAP_DAYS)),
        vol.Optional("max_interval_days"): vol.All(int, vol.Range(min=1, max=INTERVAL_DAYS_RANGE[1])),
        vol.Optional("seasonal_enabled"): bool,
        vol.Optional("sensor_prediction_enabled"): bool,
    }
)
@require_write
@websocket_api.async_response
async def ws_set_adaptive(
    hass: HomeAssistant,
    connection: websocket_api.ActiveConnection,
    msg: dict[str, Any],
) -> None:
    """Set the adaptive-scheduling tuning for a task (dialog parity with the
    options flow's adaptive step). The environmental binding has its own
    endpoint above; omitted optional fields keep their stored values."""
    from ..helpers.schedule import read_legacy_fields

    entry = _load_object_entry(hass, connection, msg)
    if entry is None:
        return

    task_id = msg["task_id"]
    tasks_data = _get_merged_tasks(entry)
    if task_id not in tasks_data:
        connection.send_error(msg["id"], "not_found", "Task not found")
        return

    adaptive_config = dict(tasks_data[task_id].get("adaptive_config", {}))
    adaptive_config["enabled"] = msg["enabled"]
    for src, dst in (
        ("ewa_alpha", "ewa_alpha"),
        ("min_interval_days", "min_interval_days"),
        ("max_interval_days", "max_interval_days"),
        ("seasonal_enabled", "seasonal_enabled"),
        ("sensor_prediction_enabled", "sensor_prediction_enabled"),
    ):
        if src in msg:
            adaptive_config[dst] = msg[src]

    min_iv = adaptive_config.get("min_interval_days")
    max_iv = adaptive_config.get("max_interval_days")
    if min_iv is not None and max_iv is not None and min_iv > max_iv:
        connection.send_error(msg["id"], "invalid_format", "min_interval_days exceeds max_interval_days")
        return

    # Same base_interval seeding as the options flow's adaptive step.
    if "base_interval" not in adaptive_config:
        base = read_legacy_fields(tasks_data[task_id])["interval_days"]
        adaptive_config["base_interval"] = base if base is not None else 30

    await _persist_adaptive_config(hass, entry, task_id, adaptive_config)

    connection.send_result(msg["id"], {"success": True, "adaptive_config": adaptive_config})
