"""Shading scene resource for the Daly Energy SDK.

Shading scenes represent 3D geometry for near-shading analysis. They are
created either from canonical JSON or by importing ``.dae`` (PVCase) / ``.shd``
files. The computation runs asynchronously via SQS.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any, BinaryIO

if TYPE_CHECKING:
    from dalysdk.client import DalyClient


class ShadingSceneResource:
    """Interact with the ``/shadingscenes`` endpoints.

    Usage::

        scenes = client.shading_scenes.list()
        scene = client.shading_scenes.get(1)

        # Import a scene file
        result = client.shading_scenes.import_file(
            file_path="scene.dae",
            name="Main Array",
            file_type="pvc",
        )

        # Create a scene from canonical JSON
        result = client.shading_scenes.create_from_json(
            name="API JSON scene",
            scene_json={"tableList": [{"outline": [[0, 0, 0], [2, 0, 0], [2, 1, 0], [0, 1, 0]]}]},
        )

        # Refresh shading calculation
        client.shading_scenes.refresh(1)

        # Set tracking inputs
        client.shading_scenes.set_tracking_inputs(1, gcr=0.35, azimuth=180.0)

        client.shading_scenes.delete(1)
    """

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

    def list(self) -> list[dict]:
        """List all shading scenes in the workspace.

        Returns scene metadata without the full scene JSON for performance.

        Returns:
            List of shading scene dicts.
        """
        return self._client._request("GET", "/shadingscenes")

    def get(self, scene_id: int) -> dict:
        """Get a shading scene by its workspace-scoped ID.

        Includes the full scene JSON.

        Args:
            scene_id: Workspace-scoped shading scene index.

        Returns:
            Shading scene dict with full geometry data.

        Raises:
            NotFoundError: If the scene does not exist.
        """
        return self._client._request("GET", f"/shadingscenes/{scene_id}")

    def create(
        self,
        name: str,
        *,
        file_path: str | Path | None = None,
        file_obj: BinaryIO | None = None,
        filename: str | None = None,
        file_type: str = "pvc",
        read_diodes: bool = True,
        gcr: float | None = None,
        azimuth: float | None = None,
        axis_tilt: float | None = None,
        back_tracking: bool | None = None,
        max_angle: float | None = None,
        terrain_aware: bool | None = None,
        terrain_aware_diode_threshold: float | None = None,
        terrain_aware_method: str | None = None,
    ) -> dict:
        """Backward-compatible alias for :meth:`import_file`.

        Historically ``create()`` uploaded a scene file directly to
        ``POST /shadingscenes``. The API now uses ``POST /shadingscenes/import``
        for that workflow, while canonical JSON creation lives at
        ``POST /shadingscenes``.
        """
        return self.import_file(
            name,
            file_path=file_path,
            file_obj=file_obj,
            filename=filename,
            file_type=file_type,
            read_diodes=read_diodes,
            gcr=gcr,
            azimuth=azimuth,
            axis_tilt=axis_tilt,
            back_tracking=back_tracking,
            max_angle=max_angle,
            terrain_aware=terrain_aware,
            terrain_aware_diode_threshold=terrain_aware_diode_threshold,
            terrain_aware_method=terrain_aware_method,
        )

    def import_file(
        self,
        name: str,
        *,
        file_path: str | Path | None = None,
        file_obj: BinaryIO | None = None,
        filename: str | None = None,
        file_type: str = "pvc",
        read_diodes: bool = True,
        gcr: float | None = None,
        azimuth: float | None = None,
        axis_tilt: float | None = None,
        back_tracking: bool | None = None,
        max_angle: float | None = None,
        terrain_aware: bool | None = None,
        terrain_aware_diode_threshold: float | None = None,
        terrain_aware_method: str | None = None,
    ) -> dict:
        """Upload a scene file and create a shading scene.

        The upload is streamed to the API import endpoint, which creates the
        scene placeholder and queues async parsing plus shading processing.
        Returns the accepted response payload with ``taskId`` for tracking.

        Provide either ``file_path`` or ``file_obj``.

        Args:
            name: Scene name.
            file_path: Path to the scene file (``.dae`` or ``.shd``) on disk.
            file_obj: Open binary stream containing scene data.
            filename: Filename for ``file_obj``. Defaults to ``"scene.dae"``.
            file_type: File format — ``"pvc"`` (PVCase .dae) or ``"shd"``.
            read_diodes: Whether to parse diode configuration. Default True.
            gcr: Ground coverage ratio (tracker parameter).
            azimuth: Tracker azimuth angle.
            axis_tilt: Tracker axis tilt.
            back_tracking: Enable backtracking. Default True when gcr is set.
            max_angle: Maximum tracking angle.
            terrain_aware: Enable terrain-aware tracking.
            terrain_aware_diode_threshold: Diode threshold for terrain-aware.
            terrain_aware_method: Method for terrain-aware tracking.

        Returns:
            Dict with ``taskId`` and scene metadata.

        Raises:
            ValueError: If neither ``file_path`` nor ``file_obj`` is provided.
        """
        # Build multipart form data
        form_data: dict[str, Any] = {
            "name": name,
            "file_type": file_type,
            "read_diodes": str(read_diodes).lower(),
        }

        # Add optional tracker parameters
        if gcr is not None:
            form_data["gcr"] = str(gcr)
        if azimuth is not None:
            form_data["azimuth"] = str(azimuth)
        if axis_tilt is not None:
            form_data["axis_tilt"] = str(axis_tilt)
        if back_tracking is not None:
            form_data["back_tracking"] = str(back_tracking).lower()
        if max_angle is not None:
            form_data["max_angle"] = str(max_angle)
        if terrain_aware is not None:
            form_data["terrain_aware"] = str(terrain_aware).lower()
        if terrain_aware_diode_threshold is not None:
            form_data["terrain_aware_diode_threshold"] = str(terrain_aware_diode_threshold)
        if terrain_aware_method is not None:
            form_data["terrain_aware_method"] = terrain_aware_method

        if file_path is not None:
            path = Path(file_path)
            with open(path, "rb") as f:
                files = {"file": (path.name, f, "application/octet-stream")}
                return self._client._request("POST", "/shadingscenes/import", data=form_data, files=files)

        if file_obj is not None:
            fname = filename or "scene.dae"
            files = {"file": (fname, file_obj, "application/octet-stream")}
            return self._client._request("POST", "/shadingscenes/import", data=form_data, files=files)

        raise ValueError("Provide either file_path or file_obj")

    def create_from_json(
        self,
        name: str,
        scene_json: dict,
        *,
        scene_source: str = "api-json",
        tracking_parameters: dict | None = None,
    ) -> dict:
        """Create a shading scene directly from canonical scene JSON.

        Args:
            name: Scene name.
            scene_json: Canonical shading scene payload (tableList, obstructions, modelInputs).
            scene_source: Source label persisted on the scene record.
            tracking_parameters: Optional tracking configuration payload.

        Returns:
            Dict with scene/task info (async processing started).
        """
        payload: dict[str, Any] = {
            "name": name,
            "sceneJson": scene_json,
            "sceneSource": scene_source,
        }
        if tracking_parameters is not None:
            payload["trackingParameters"] = tracking_parameters
        return self._client._request("POST", "/shadingscenes", json=payload)

    def get_results(self, scene_id: int, *, include_status: bool = False) -> dict:
        """Get consolidated shading results for a scene.

        Args:
            scene_id: Workspace-scoped shading scene index.
            include_status: When true, request the richer authoritative status
                envelope from ``/results?includeStatus=true``.

        Returns:
            Dict with the consolidated shading results, or a pending payload
            such as ``{"status": "pending", "results": None}`` when results
            are not yet available.

        Raises:
            NotFoundError: If the scene does not exist.
        """
        params = {"includeStatus": "true"} if include_status else None
        return self._client._request("GET", f"/shadingscenes/{scene_id}/results", params=params)

    def get_results_status(self, scene_id: int) -> dict:
        """Get the authoritative shading status envelope for a scene."""
        return self.get_results(scene_id, include_status=True)

    def get_progress(self, scene_id: int) -> dict:
        """Return a compact progress summary derived from true API counts."""
        payload = self.get_results_status(scene_id)
        completed_positions = int(payload.get("completedPositions") or 0)
        expected_positions = int(payload.get("expectedPositions") or 0)
        ready = payload.get("status") == "completed" or (
            expected_positions > 0 and completed_positions >= expected_positions
        )
        return {
            "status": payload.get("status"),
            "ready": ready,
            "completedPositions": completed_positions,
            "runningPositions": int(payload.get("runningPositions") or 0),
            "failedPositions": int(payload.get("failedPositions") or 0),
            "expectedPositions": expected_positions,
            "remainingPositions": int(payload.get("remainingPositions") or 0),
            "taskId": payload.get("taskId"),
            "taskStatus": payload.get("taskStatus"),
            "progress": payload.get("progress"),
            "stage": payload.get("stage"),
            "error": payload.get("error"),
        }

    def get_gcr(self, scene_id: int) -> dict | None:
        """Get computed GCR properties for a shading scene.

        Returns dict with has_trackers, tables_processed, total_table_area,
        gcr, avg_collector_width, axis_tilt. Returns None if scene has no
        GCR data (e.g. fixed-tilt).

        Args:
            scene_id: Workspace-scoped shading scene index.

        Returns:
            GCR properties dict, or None if not available.

        Raises:
            NotFoundError: If the scene does not exist.
        """
        response = self._client._send("GET", f"/shadingscenes/{scene_id}/gcr")
        if response.status_code == 404:
            return None
        return self._client._handle_response(response)

    def refresh(self, scene_id: int) -> dict:
        """Re-run shading calculation for an existing scene.

        Clears existing results and queues a new shading computation.

        Args:
            scene_id: Workspace-scoped shading scene index.

        Returns:
            Dict with ``task_id`` for the new computation.

        Raises:
            NotFoundError: If the scene does not exist.
        """
        return self._client._request("POST", f"/shadingscenes/{scene_id}/refresh")

    def set_tracking_inputs(
        self,
        scene_id: int,
        gcr: float,
        *,
        azimuth: float = 0.0,
        axis_tilt: float = 0.0,
        back_tracking: bool = True,
        max_angle: float = 60.0,
        terrain_aware: bool = False,
        terrain_aware_diode_threshold: float | None = None,
        terrain_aware_method: str | None = None,
    ) -> dict:
        """Set tracking angle configuration for a shading scene.

        Args:
            scene_id: Workspace-scoped shading scene index.
            gcr: Ground coverage ratio (required).
            azimuth: Tracker azimuth angle. Default 0.0.
            axis_tilt: Tracker axis tilt. Default 0.0.
            back_tracking: Enable backtracking. Default True.
            max_angle: Maximum tracking angle. Default 60.0.
            terrain_aware: Enable terrain-aware tracking. Default False.
            terrain_aware_diode_threshold: Diode threshold for terrain-aware.
            terrain_aware_method: Method for terrain-aware tracking.

        Returns:
            Dict with ``trackingInputId`` and ``shadingSceneId``.

        Raises:
            NotFoundError: If the scene does not exist.
        """
        form_data: dict[str, Any] = {
            "gcr": str(gcr),
            "azimuth": str(azimuth),
            "axis_tilt": str(axis_tilt),
            "back_tracking": str(back_tracking).lower(),
            "max_angle": str(max_angle),
            "terrain_aware": str(terrain_aware).lower(),
        }
        if terrain_aware_diode_threshold is not None:
            form_data["terrain_aware_diode_threshold"] = str(terrain_aware_diode_threshold)
        if terrain_aware_method is not None:
            form_data["terrain_aware_method"] = terrain_aware_method

        return self._client._request("POST", f"/shadingscenes/{scene_id}/trackinginputs", data=form_data)

    def delete(self, scene_id: int) -> dict:
        """Delete a shading scene.

        Args:
            scene_id: Workspace-scoped shading scene index.

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

        Raises:
            NotFoundError: If the scene does not exist.
        """
        return self._client._request("DELETE", f"/shadingscenes/{scene_id}")
