"""Energy model resource for the Daly Energy SDK.

Energy model submissions are async-only on ``POST /energymodels`` and
``POST /energymodels/{id}/run``. The SDK keeps compatibility parameters for
older call sites, but the helpers always submit the full model payload and
return the accepted task envelope.
"""

from __future__ import annotations

from copy import deepcopy
from typing import TYPE_CHECKING, Literal

from dalysdk.querying import build_query_payload

if TYPE_CHECKING:
    from collections.abc import Iterable

    from dalysdk.client import DalyClient

ListIncludeToken = Literal["summary"]
DetailIncludeToken = Literal["summary", "inputs", "fulltimeseries", "blocktimeseries"]
CreateIncludeToken = Literal["summary", "inputs", "fulltimeseries", "blocktimeseries"]

_LIST_INCLUDE_ALLOWED = {"summary"}
_DETAIL_INCLUDE_ALLOWED = {"summary", "inputs", "fulltimeseries", "blocktimeseries"}
_CREATE_INCLUDE_ALLOWED = {"summary", "inputs", "fulltimeseries", "blocktimeseries"}


class EnergyModelResource:
    """Interact with the ``/energymodels`` submission endpoints.

    Usage::

        # List saved runs
        models = client.energy_models.list()
        models_with_summary = client.energy_models.list(include="summary")

        # Get a specific run
        model = client.energy_models.get(1, include=["summary", "inputs"])

        # Submit a new energy model (returns the accepted task envelope)
        result = client.energy_models.create(epm_input_dict, include="summary")

        # Load the saved input JSON for editing
        editable = client.energy_models.get_with_inputs(1)

        # Update and rerun the same saved row in place
        updated = client.energy_models.update(1, editable["inputs"])
        rerun = client.energy_models.run_saved(1)

        client.energy_models.delete(1)
    """

    def __init__(self, client: DalyClient):
        self._client = client

    @staticmethod
    def _request_timeout(*, async_mode: bool, timeout: float | None) -> float | None:
        del async_mode
        return timeout

    def list(self, *, include: ListIncludeToken | Iterable[ListIncludeToken] | None = None) -> list[dict]:
        """List saved energy model runs.

        Args:
            include: Optional include token(s). ``list`` supports only
                     ``"summary"``.

        Returns:
            List of energy model dicts.
        """
        params: dict[str, str] = {}
        include_value = _normalize_include(include, allowed=_LIST_INCLUDE_ALLOWED)
        if include_value is not None:
            params["include"] = include_value
        return self._client._request("GET", "/energymodels", params=params)

    def get(
        self,
        energy_model_id: int,
        *,
        include: DetailIncludeToken | Iterable[DetailIncludeToken] | None = None,
    ) -> dict:
        """Get an energy model run by its workspace-scoped ID.

        Args:
            energy_model_id: Workspace-scoped energy model (run) index.
            include: Optional include token(s). ``get`` supports
                     ``"summary"``, ``"inputs"``, ``"fulltimeseries"``,
                     and ``"blocktimeseries"``.

        Returns:
            Energy model dict.

        Raises:
            NotFoundError: If the energy model does not exist.
        """
        params: dict[str, str] = {}
        include_value = _normalize_include(include, allowed=_DETAIL_INCLUDE_ALLOWED)
        if include_value is not None:
            params["include"] = include_value
        return self._client._request("GET", f"/energymodels/{energy_model_id}", params=params)

    def get_with_inputs(self, energy_model_id: int) -> dict:
        """Get an energy model with its persisted saved input JSON.

        This is the canonical retrieval helper for edit/rerun flows and maps to
        ``GET /energymodels/{energy_model_id}?include=inputs``.
        """
        return self.get(energy_model_id, include="inputs")

    def create(
        self,
        data: dict,
        *,
        async_mode: bool = True,
        timeout: float | None = None,
        include: CreateIncludeToken | Iterable[CreateIncludeToken] | None = None,
    ) -> dict:
        """Submit an energy production model.

        The API is async-only for submissions. ``async_mode`` is retained only
        for backward compatibility with older call sites and does not change
        request behavior.

        Args:
            data: Full energy model input JSON (location, blocks, plant,
                  soiling, output configuration). Canonical time-series
                  controls are carried in ``data["output"]`` such as
                  ``timeSeries``, ``fullTimeSeries``, ``blockResults``,
                  ``blockIndex``, ``lossBreakdownTimestamps``, and
                  ``irradianceLossDetail``.
            async_mode: Compatibility input retained for older SDK callers.
                        It is ignored.
            timeout: Optional request timeout override in seconds.
            include: Deprecated compatibility parameter. The API does not
                     support ``include`` on ``POST /energymodels``, so the SDK
                     ignores it instead of forwarding an invalid query param.

        Returns:
            Raw API response dict containing the accepted submission envelope.

        Example — query mode (inline location + weather, no database records)::

            result = client.energy_models.create({
                "location": {
                    "latitude": 33.4,
                    "longitude": -112.0,
                    "elevation": 337,
                    "timeZone": -7,
                },
                "weatherData": {
                    "ghi": [...],         # 8760 values
                    "dhi": [...],
                    "windSpeed": [...],
                    "temperature": [...],
                },
                "blocks": [...]
            })
        """
        _normalize_create_include(include)
        kwargs: dict = {
            "json": data,
            "timeout": self._request_timeout(async_mode=async_mode, timeout=timeout),
        }
        return self._client._request("POST", "/energymodels", **kwargs)

    def build_query_payload(
        self,
        base_input: dict,
        weather_data: object,
        *,
        location: dict | None = None,
        start_date: object | None = None,
        end_date: object | None = None,
        time_step_format: Literal["utc", "offset", "local"] = "utc",
        timezone_offset: float | None = None,
        timezone_name: str | None = None,
    ) -> dict:
        """Build an inline-weather query payload for submission to ``POST /energymodels``.

        This is the canonical SDK helper for query-mode energy models. It:
        - accepts weather input as a DataFrame, list of records, or dict of arrays
        - filters an optional query window
        - normalizes dates to epoch milliseconds
        - emits top-level ``weatherData`` arrays ready for the API
        """
        return build_query_payload(
            deepcopy(base_input),
            weather_data,
            location=location,
            start_date=start_date,
            end_date=end_date,
            time_step_format=time_step_format,
            timezone_offset=timezone_offset,
            timezone_name=timezone_name,
        )

    def query(
        self,
        base_input: dict,
        weather_data: object,
        *,
        location: dict | None = None,
        start_date: object | None = None,
        end_date: object | None = None,
        time_step_format: Literal["utc", "offset", "local"] = "utc",
        timezone_offset: float | None = None,
        timezone_name: str | None = None,
        async_mode: bool = False,
        timeout: float | None = None,
    ) -> dict:
        """Build and submit a query-mode energy model with inline weather data.

        ``async_mode`` is kept for compatibility with older SDK callers and is
        ignored.
        """
        del async_mode
        payload = self.build_query_payload(
            base_input,
            weather_data,
            location=location,
            start_date=start_date,
            end_date=end_date,
            time_step_format=time_step_format,
            timezone_offset=timezone_offset,
            timezone_name=timezone_name,
        )
        return self.create(payload, timeout=timeout)

    def update(self, energy_model_id: int, data: dict) -> dict:
        """Update the saved input JSON for an existing energy model row.

        Maps to ``PUT /energymodels/{energy_model_id}``.

        The API validates the full ``EnergyModelInput`` payload, replaces the
        saved JSON on the existing row, clears stale outputs, and returns the
        updated detail payload including ``inputs``.
        """
        return self._client._request("PUT", f"/energymodels/{energy_model_id}", json=data)

    def run_saved(
        self,
        energy_model_id: int,
        *,
        async_mode: bool = False,
        timeout: float | None = None,
    ) -> dict:
        """Submit the currently saved input JSON on an existing energy model row.

        Maps to ``POST /energymodels/{energy_model_id}/run`` and reuses the
        same energy-model row in place rather than creating a new one.

        Args:
            energy_model_id: Workspace-scoped energy model (run) index.
            async_mode: Compatibility input retained for older SDK callers.
                        It is ignored.
            timeout: Optional request timeout override in seconds.

        Note:
            The current API reruns the saved row's stored input as-is and does
            not accept a rerun request body. To change output controls or other
            inputs before rerunning, call ``update()`` first and then
            ``run_saved()``.
        """
        kwargs: dict = {
            "timeout": self._request_timeout(async_mode=async_mode, timeout=timeout),
        }
        return self._client._request("POST", f"/energymodels/{energy_model_id}/run", **kwargs)

    def create_legacy_query(
        self,
        data: dict,
        *,
        async_mode: bool = False,
        block_results: bool = False,
        block_results_index: int = -1,
        timeout: float | None = None,
        include: CreateIncludeToken | Iterable[CreateIncludeToken] | None = None,
    ) -> dict:
        """Legacy-compatible energy-model submission helper.

        Preserves historical SDK query behavior:
        - ``block_results=True`` sets query param ``blockresults=true``
        - ``block_results_index >= 0`` injects top-level ``blockQuery`` and
          ``blockQueryIndex`` fields

        ``async_mode`` is retained for compatibility with older SDK callers
        and is ignored.

        The current API treats block selection as block-results filtering, not
        single-block execution. The SDK therefore keeps the full ``blocks``
        list intact and relies on the API compatibility layer to translate the
        deprecated request fields. Public responses preserve the API's original
        zero-based ``blockTimeSeries`` keys.
        """
        payload = dict(data)
        if block_results and block_results_index >= 0:
            blocks = payload.get("blocks")
            if not isinstance(blocks, list) or not blocks:
                raise ValueError("Legacy block query requires a non-empty 'blocks' list.")
            if block_results_index >= len(blocks):
                raise ValueError(
                    f"Legacy block_results_index {block_results_index} out of range for blocks length {len(blocks)}."
                )
            payload["blockQuery"] = True
            payload["blockQueryIndex"] = block_results_index

        params: dict[str, str] = {"blockresults": str(block_results).lower()}
        _normalize_create_include(include)
        kwargs: dict = {
            "json": payload,
            "params": params,
            "timeout": self._request_timeout(async_mode=async_mode, timeout=timeout),
        }
        return self._client._request("POST", "/energymodels", **kwargs)

    def delete(self, energy_model_id: int) -> dict:
        """Delete an energy model run.

        Args:
            energy_model_id: Workspace-scoped energy model (run) index.

        Returns:
            Confirmation dict (``{"deleted": True}``).

        Raises:
            NotFoundError: If the energy model does not exist.
        """
        return self._client._request("DELETE", f"/energymodels/{energy_model_id}")


def _normalize_include(include: str | Iterable[str] | None, *, allowed: set[str]) -> str | None:
    if include is None:
        return None

    tokens = [include] if isinstance(include, str) else list(include)
    if not tokens:
        return None

    deduped: list[str] = []
    for token in tokens:
        if token not in allowed:
            allowed_csv = ", ".join(sorted(allowed))
            raise ValueError(f"Invalid include token '{token}'. Allowed: {allowed_csv}")
        if token not in deduped:
            deduped.append(token)

    return ",".join(deduped)


def _normalize_create_include(include: str | Iterable[str] | None) -> None:
    if include is None:
        return
    _normalize_include(include, allowed=_CREATE_INCLUDE_ALLOWED)
