"""STL Thumbnail Generation Service.

Generates thumbnail images from STL files using trimesh and matplotlib.
"""

import logging
import os
import uuid
from pathlib import Path

logger = logging.getLogger(__name__)

# Matplotlib's font_manager emits one INFO line per font on first import
# while it builds its cache, including a noisy "Failed to extract font
# properties from NotoColorEmoji.ttf" for the COLR/COLR1 emoji format it
# doesn't support. These are not actionable — demote to WARNING so real
# font issues still surface but the first STL upload doesn't produce a
# multi-line matplotlib preamble in the journal.
logging.getLogger("matplotlib.font_manager").setLevel(logging.WARNING)


def _configure_matplotlib_cache() -> None:
    """Point matplotlib's config/cache directory at a writable persistent path.

    Without this, matplotlib falls back to ``/tmp/matplotlib-XXXXXX`` whenever
    ``$HOME/.config/matplotlib`` isn't writable — which is the case under
    Bambuddy's container / systemd-service deployments where ``$HOME`` is set
    to a non-writable path. The fallback emits a WARNING on every cold start
    AND loses the font cache on host reboot, so font_manager rebuilds it
    every time → another batch of INFO lines.

    Setting ``MPLCONFIGDIR`` to ``settings.base_dir / .cache / matplotlib``
    eliminates both: the warning never fires, and the cache survives across
    restarts so the per-font scan only runs once per deployment.
    Idempotent — respects an externally-set ``MPLCONFIGDIR`` if the operator
    chose their own path.
    """
    if os.environ.get("MPLCONFIGDIR"):
        return
    try:
        from backend.app.core.config import settings

        cache_dir = Path(settings.base_dir) / ".cache" / "matplotlib"
        cache_dir.mkdir(parents=True, exist_ok=True)
        os.environ["MPLCONFIGDIR"] = str(cache_dir)
    except Exception as exc:
        # Best-effort. If settings isn't importable or the mkdir fails (read-only
        # FS, permission denied), let matplotlib fall back to /tmp with its
        # built-in warning — same as today's behaviour, no worse.
        logger.debug("Could not configure MPLCONFIGDIR: %s", exc)


# Bambu green color for rendering
BAMBU_GREEN = "#00AE42"
BACKGROUND_COLOR = "#1a1a1a"

# Maximum vertices before simplification
MAX_VERTICES = 100000

# Minimum STL file size that could possibly contain a usable mesh:
# - Binary STL with one triangle: 80B header + 4B count + 50B triangle = 134B
# - ASCII STL with one triangle: header + "facet ... endfacet" + footer ≈ 150B
# Files below this are stubs / placeholders / corrupted; trimesh would return an
# empty mesh anyway. Pre-skipping at the call sites suppresses the warning storm
# bulk-uploaded ZIPs of small test STLs used to produce.
MIN_USABLE_STL_BYTES = 200


def generate_stl_thumbnail(
    stl_path: Path,
    thumbnails_dir: Path,
    size: int = 256,
) -> str | None:
    """Generate a thumbnail image from an STL file.

    Args:
        stl_path: Path to the STL file
        thumbnails_dir: Directory to save the thumbnail
        size: Thumbnail size in pixels (default 256x256)

    Returns:
        Path to the generated thumbnail, or None on failure
    """
    # Callers historically pass either Path or str; coerce so the `thumbnails_dir
    # / thumb_filename` join at the end of this function can't fail with the
    # str-divided-by-str TypeError (see #1299).
    stl_path = Path(stl_path)
    thumbnails_dir = Path(thumbnails_dir)

    try:
        # Must precede the matplotlib import — MPLCONFIGDIR is read at
        # matplotlib import time, not on subsequent attribute access.
        _configure_matplotlib_cache()

        import matplotlib
        import trimesh

        # Use Agg backend for headless rendering
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt
        from mpl_toolkits.mplot3d import Axes3D  # noqa: F401
        from mpl_toolkits.mplot3d.art3d import Poly3DCollection

        # Load the STL file
        mesh = trimesh.load(str(stl_path), force="mesh")

        if mesh is None or not hasattr(mesh, "vertices") or len(mesh.vertices) == 0:
            # Demoted from warning to debug: this is a per-file content
            # observation (the STL is empty / stub / corrupted), not an
            # actionable error. The caller proceeds correctly with no
            # thumbnail. The call sites also pre-skip files below
            # MIN_USABLE_STL_BYTES so the common stub-STL case never gets
            # this far — this branch now catches only the rare "large
            # enough but trimesh still can't parse it" case.
            logger.debug("Failed to load STL or empty mesh: %s", stl_path)
            return None

        # Simplify large meshes for performance
        if len(mesh.vertices) > MAX_VERTICES:
            logger.info("Simplifying mesh from %s vertices", len(mesh.vertices))
            try:
                # Calculate reduction ratio (0-1 range)
                # e.g., 124633 vertices -> 100000 means keep ~80%, so reduce by ~20%
                keep_ratio = MAX_VERTICES / len(mesh.vertices)
                target_reduction = 1.0 - keep_ratio
                # Clamp to valid range (0.01 to 0.99)
                target_reduction = max(0.01, min(0.99, target_reduction))
                mesh = mesh.simplify_quadric_decimation(target_reduction)
                logger.info("Simplified mesh to %s vertices", len(mesh.vertices))
            except Exception as e:
                logger.warning("Mesh simplification failed, using original: %s", e)

        # Get mesh bounds and center it
        vertices = mesh.vertices
        bounds_min = vertices.min(axis=0)
        bounds_max = vertices.max(axis=0)
        center = (bounds_min + bounds_max) / 2
        vertices_centered = vertices - center

        # Scale to fit in view
        max_extent = (bounds_max - bounds_min).max()
        if max_extent > 0:
            scale = 1.0 / max_extent
            vertices_scaled = vertices_centered * scale
        else:
            vertices_scaled = vertices_centered

        # Create figure with dark background
        fig = plt.figure(figsize=(size / 100, size / 100), dpi=100)
        fig.patch.set_facecolor(BACKGROUND_COLOR)

        ax = fig.add_subplot(111, projection="3d")
        ax.set_facecolor(BACKGROUND_COLOR)

        # Create polygon collection from mesh faces
        faces = mesh.faces
        poly3d = [[vertices_scaled[vertex] for vertex in face] for face in faces]

        collection = Poly3DCollection(
            poly3d,
            facecolors=BAMBU_GREEN,
            edgecolors=BAMBU_GREEN,
            linewidths=0.1,
            alpha=0.9,
        )
        ax.add_collection3d(collection)

        # Set axis limits
        ax.set_xlim(-0.6, 0.6)
        ax.set_ylim(-0.6, 0.6)
        ax.set_zlim(-0.6, 0.6)

        # Set view angle (isometric-ish)
        ax.view_init(elev=25, azim=45)

        # Remove axes and grid
        ax.set_axis_off()
        ax.grid(False)

        # Remove margins
        plt.subplots_adjust(left=0, right=1, top=1, bottom=0)

        # Save thumbnail
        thumb_filename = f"{uuid.uuid4().hex}.png"
        thumb_path = thumbnails_dir / thumb_filename  # SEC-PATH-OK: thumb_filename = uuid.uuid4().hex + ".png"

        fig.savefig(
            thumb_path,
            format="png",
            facecolor=BACKGROUND_COLOR,
            edgecolor="none",
            bbox_inches="tight",
            pad_inches=0.05,
            dpi=100,
        )
        plt.close(fig)

        logger.info("Generated STL thumbnail: %s", thumb_path)
        return str(thumb_path)

    except ImportError as e:
        logger.warning("STL thumbnail generation unavailable (missing dependencies): %s", e)
        return None
    except Exception as e:
        # Log the traceback, not just the message: a bare
        # "unsupported operand type(s) for /: 'str' and 'str'" gives no clue
        # which line failed, and the fault is data-/environment-specific
        # enough that it can't be reproduced from a clean STL — the traceback
        # in the next support bundle is what pinpoints it (#1480).
        logger.warning("Failed to generate STL thumbnail for %s: %s", stl_path, e, exc_info=True)
        return None
