"""render_3d — render preview images from an STL file using OpenSCAD.

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 .base import Tool, ToolResult

# Camera presets: (rot_x, rot_y, rot_z)
_CAMERA_PRESETS: dict[str, tuple[int, int, int]] = {
    "front": (0, 0, 0),
    "back": (0, 0, 180),
    "top": (90, 0, 0),
    "bottom": (-90, 0, 0),
    "left": (0, 0, -90),
    "right": (0, 0, 90),
    "iso": (55, 0, 45),
}

_IMG_W, _IMG_H = 400, 300
_MAX_VIEWS = 3


class Render3DTool(Tool):
    name = "render_3d"
    description = (
        "Render preview images from an STL file using OpenSCAD. "
        "Produces one PNG per requested view.\n\n"
        "Workflow:\n"
        "1. Generate the STL with model_3d.\n"
        "2. Call render_3d with the STL path and desired views.\n"
        "3. PNG files are saved next to the STL with view suffixes.\n"
        "4. Inspect PNG previews with image_view before publishing the final 3D model. "
        "PNG previews are usually internal QA artifacts, not user-facing deliverables.\n\n"
        "Available views: front, back, top, bottom, left, right, iso."
    )
    parameters = {
        "type": "object",
        "properties": {
            "source": {
                "type": "string",
                "description": "Path to the STL file to render. Must exist.",
            },
            "views": {
                "type": "array",
                "items": {"type": "string", "enum": list(_CAMERA_PRESETS.keys())},
                "description": (
                    "List of camera views to render. "
                    f"Maximum {_MAX_VIEWS} views per call. "
                    "Default: [\"iso\"]"
                ),
            },
        },
        "required": ["source"],
    }

    async def execute(self, params: dict) -> ToolResult:
        if not shutil.which("openscad"):
            return ToolResult(
                success=False,
                output="OpenSCAD is not installed on this system.",
                error="openscad_not_found",
            )

        source = Path(params["source"]).expanduser().resolve()
        if not source.exists():
            return ToolResult(
                success=False,
                output=f"STL file not found: {source}",
                error="stl_not_found",
            )
        if not source.is_file():
            return ToolResult(
                success=False,
                output=f"Path is not a file: {source}",
                error="not_a_file",
            )

        views = params.get("views") or ["iso"]
        if len(views) > _MAX_VIEWS:
            return ToolResult(
                success=False,
                output=f"Too many views: {len(views)} (max {_MAX_VIEWS}).",
                error="too_many_views",
            )

        invalid = [v for v in views if v not in _CAMERA_PRESETS]
        if invalid:
            return ToolResult(
                success=False,
                output=f"Unknown views: {invalid}. Available: {list(_CAMERA_PRESETS.keys())}.",
                error="invalid_views",
            )

        generated: list[str] = []
        errors: list[str] = []

        for view in views:
            rot_x, rot_y, rot_z = _CAMERA_PRESETS[view]
            out_png = source.with_suffix(f".{view}.png")

            # Build a temporary SCAD that imports the STL
            tmp_scad = source.with_suffix(f".{view}.tmp.scad")
            tmp_scad.write_text(f'import("{source}");\n', encoding="utf-8")

            proc = await asyncio.create_subprocess_exec(
                "openscad",
                "--camera", f"0,0,0,{rot_x},{rot_y},{rot_z},500",
                "--autocenter",
                "--viewall",
                "--imgsize", f"{_IMG_W},{_IMG_H}",
                "--preview",
                "-o", str(out_png),
                str(tmp_scad),
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await proc.communicate()

            # Clean up temp SCAD immediately
            try:
                tmp_scad.unlink()
            except Exception:
                pass

            if proc.returncode != 0:
                err = stderr.decode(errors="replace").strip() or "OpenSCAD render error"
                errors.append(f"{view}: {err}")
                continue

            if out_png.exists():
                generated.append(str(out_png))
            else:
                errors.append(f"{view}: no PNG produced")

        if errors and not generated:
            return ToolResult(
                success=False,
                output="All renders failed:\n" + "\n".join(errors),
                error="render_failed",
            )

        lines = [f"Generated {len(generated)} image(s):"]
        for p in generated:
            lines.append(f"  {Path(p).name}")
        if errors:
            lines.append("\nErrors:")
            for e in errors:
                lines.append(f"  {e}")

        return ToolResult(
            success=bool(generated),
            output="\n".join(lines),
            metadata={"generated": generated},
        )
