"""model_3d — generate an STL file from an OpenSCAD script.

Requires OpenSCAD installed on the system:
  Arch:    sudo pacman -S openscad
  Debian:  sudo apt install openscad
"""

import asyncio
import shutil
from pathlib import Path

from navi.config import settings
from navi.session_files import session_dir

from .base import Tool, ToolResult, current_session_id


class Model3DTool(Tool):
    name = "model_3d"
    description = (
        "Generate a binary STL file from an existing OpenSCAD (.scad) script. "
        "The script must already be written to disk; this tool only compiles it.\n\n"
        "Workflow:\n"
        "1. Write your .scad script with the filesystem tool.\n"
        "2. Call model_3d with the scad_path and desired output_path.\n"
        "3. The resulting STL is saved to output_path.\n\n"
        "Session files rule: if you use session_files/<session_id>/..., use the exact current "
        "session directory from context or tool output. Do not reconstruct or retype session IDs. "
        "Simple filenames without directories are resolved inside the current session directory.\n\n"
        "Use content_publish on the STL to show the user an interactive 3D viewer."
    )
    parameters = {
        "type": "object",
        "properties": {
            "scad_path": {
                "type": "string",
                "description": (
                    "Absolute or relative path to the existing .scad file. "
                    "The file must already exist."
                ),
            },
            "output_path": {
                "type": "string",
                "description": (
                    "Absolute or relative path where the STL should be written. "
                    "Parent directories are created automatically."
                ),
            },
        },
        "required": ["scad_path", "output_path"],
    }

    async def execute(self, params: dict) -> ToolResult:
        session_id = current_session_id.get()
        scad_path = self._resolve_model_path(params["scad_path"], session_id)
        output_path = self._resolve_model_path(params["output_path"], session_id)

        session_error = self._validate_current_session_path(scad_path, "scad_path", session_id)
        if session_error:
            return session_error
        session_error = self._validate_current_session_path(output_path, "output_path", session_id)
        if session_error:
            return session_error

        if not scad_path.exists():
            return ToolResult(
                success=False,
                output=f"SCAD file not found: {scad_path}",
                error="scad_not_found",
            )
        if not scad_path.is_file():
            return ToolResult(
                success=False,
                output=f"Path is not a file: {scad_path}",
                error="not_a_file",
            )

        if not shutil.which("openscad"):
            return ToolResult(
                success=False,
                output="OpenSCAD is not installed on this system.",
                error="openscad_not_found",
            )

        output_path.parent.mkdir(parents=True, exist_ok=True)

        proc = await asyncio.create_subprocess_exec(
            "openscad",
            "--export-format", "binstl",
            "-o", str(output_path),
            str(scad_path),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await proc.communicate()

        if proc.returncode != 0:
            err = (stderr.decode(errors="replace") or "OpenSCAD exited with an error.").strip()
            return ToolResult(
                success=False,
                output=f"OpenSCAD failed to compile STL:\n{err}",
                error="openscad_compile_error",
            )

        if not output_path.exists():
            return ToolResult(
                success=False,
                output="OpenSCAD completed but no STL file was produced.",
                error="no_output",
            )

        size_kb = output_path.stat().st_size / 1024
        return ToolResult(
            success=True,
            output=(
                f"Generated: {output_path.name}\n"
                f"Path: {output_path}\n"
                f"Size: {size_kb:.1f} KB"
            ),
            metadata={"output_path": str(output_path), "size_kb": round(size_kb, 1)},
        )

    @staticmethod
    def _resolve_model_path(raw_path: str, session_id: str | None) -> Path:
        path = Path(raw_path).expanduser()
        if session_id and not path.is_absolute() and len(path.parts) == 1:
            return (session_dir(session_id) / path).resolve()
        return path.resolve()

    @staticmethod
    def _validate_current_session_path(
        path: Path,
        param_name: str,
        session_id: str | None,
    ) -> ToolResult | None:
        if not session_id:
            return None

        session_root = Path(settings.session_files_dir).resolve()
        current_dir = session_dir(session_id).resolve()
        if path.is_relative_to(session_root) and not path.is_relative_to(current_dir):
            return ToolResult(
                success=False,
                output=(
                    f"{param_name} points to a different session directory: {path}\n"
                    f"Current session directory is: {current_dir}\n"
                    "Use the exact current session directory. Do not manually reconstruct "
                    "or retype session IDs."
                ),
                error="wrong_session_dir",
            )
        return None
