#!/usr/bin/env python3
"""Score an angled-slot assembly-fixture STEP model against the private reference."""

from __future__ import annotations

import argparse
import importlib.util
import json
import os
import stat
import tempfile
from pathlib import Path
from typing import Any

SCRIPTS_DIR = Path(__file__).resolve().parent
MIN_FILE_SIZE = 1024
MAX_FILE_SIZE = 25 * 1024 * 1024
FORBIDDEN_EXTERNAL_REFERENCE_MARKERS = (
    b"DOCUMENT_FILE",
    b"EXTERNAL_SOURCE",
    b"EXTERNALLY_DEFINED_",
    b"FILE:",
    b"FILE://",
    b"FTP://",
    b"HTTP://",
    b"HTTPS://",
    b"/TESTS/",
    b"\\TESTS\\",
    b"/SOLUTION/",
    b"\\SOLUTION\\",
    b"TESTS/REFERENCE",
    b"SOLUTION/",
)


def load_geometry_compare():
    path = SCRIPTS_DIR / "geometry_compare.py"
    spec = importlib.util.spec_from_file_location("private_geometry_compare", path)
    if spec is None or spec.loader is None:
        raise RuntimeError("failed to load the private geometry comparator")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def snapshot_submission(path: Path, destination: Path) -> dict[str, Any]:
    """Safely snapshot one bounded regular file without following symlinks."""

    flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
    descriptor = os.open(path, flags)
    try:
        metadata = os.fstat(descriptor)
        if not stat.S_ISREG(metadata.st_mode):
            raise ValueError("submission is not a regular file")
        if not MIN_FILE_SIZE <= metadata.st_size <= MAX_FILE_SIZE:
            raise ValueError(
                f"STEP file size must be between {MIN_FILE_SIZE} and {MAX_FILE_SIZE} bytes"
            )
        chunks: list[bytes] = []
        remaining = MAX_FILE_SIZE + 1
        while remaining:
            chunk = os.read(descriptor, min(1024 * 1024, remaining))
            if not chunk:
                break
            chunks.append(chunk)
            remaining -= len(chunk)
        data = b"".join(chunks)
    finally:
        os.close(descriptor)

    if not MIN_FILE_SIZE <= len(data) <= MAX_FILE_SIZE:
        raise ValueError("STEP file changed size while it was being read")

    upper = data.upper()
    if not upper.lstrip().startswith(b"ISO-10303-21;"):
        raise ValueError("submission is not an ISO-10303-21 STEP exchange file")
    if not upper.rstrip().endswith(b"END-ISO-10303-21;"):
        raise ValueError("STEP exchange file is truncated or has trailing data")
    for marker in FORBIDDEN_EXTERNAL_REFERENCE_MARKERS:
        if marker in upper:
            raise ValueError("STEP file contains a forbidden external reference")

    destination.write_bytes(data)
    return {"bytes": len(data)}


def write_diagnostic_report(path: Path, report: dict[str, Any]) -> None:
    """Write diagnostics without following a pre-existing special file."""

    path.parent.mkdir(parents=True, exist_ok=True)
    flags = (
        os.O_WRONLY
        | os.O_CREAT
        | os.O_TRUNC
        | getattr(os, "O_CLOEXEC", 0)
        | getattr(os, "O_NOFOLLOW", 0)
        | getattr(os, "O_NONBLOCK", 0)
    )
    descriptor = os.open(path, flags, 0o600)
    try:
        if not stat.S_ISREG(os.fstat(descriptor).st_mode):
            raise ValueError("diagnostic report destination is not a regular file")
        payload = (json.dumps(report, indent=2) + "\n").encode("utf-8")
        offset = 0
        while offset < len(payload):
            written = os.write(descriptor, payload[offset:])
            if written < 1:
                raise OSError("failed to write diagnostic report")
            offset += written
    finally:
        os.close(descriptor)


def score(
    submission: Path,
    reference: Path,
    report_path: Path,
) -> dict[str, Any]:
    try:
        with tempfile.TemporaryDirectory(prefix="assembly-fixture-verify-") as temp_dir:
            snapshot = Path(temp_dir) / "submission.step"
            file_info = snapshot_submission(submission, snapshot)
            comparator = load_geometry_compare()
            report = comparator.compare(reference, snapshot)
    except Exception as error:  # noqa: BLE001 - malformed submissions must score zero
        return {
            "score": 0.0,
            "passed": False,
            "status": "ERROR",
            "error": str(error),
        }

    try:
        write_diagnostic_report(report_path, report)
    except (OSError, ValueError):
        # The detailed report is diagnostic only; it must not alter the score
        # when the local caller does not own /logs/verifier.
        pass
    candidate = report.get("b", {})
    passed = report["status"] == "SAME"
    return {
        "score": 1.0 if passed else 0.0,
        "passed": passed,
        "status": report["status"],
        "file": file_info,
        "candidate": {
            "valid_brep": candidate.get("valid_brep"),
            "solid_count": candidate.get("solid_count"),
            "extra_face_count_outside_only_solid": candidate.get(
                "extra_face_count_outside_only_solid"
            ),
            "extra_edge_count_outside_only_solid": candidate.get(
                "extra_edge_count_outside_only_solid"
            ),
            "extra_vertex_count_outside_only_solid": candidate.get(
                "extra_vertex_count_outside_only_solid"
            ),
            "maximum_topological_tolerance_mm": candidate.get("maximum_topological_tolerance_mm"),
        },
        "message": (
            "submitted STEP is geometrically equivalent to the reference"
            if passed
            else "submitted STEP geometry does not match the reference"
        ),
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--submission", type=Path, required=True)
    parser.add_argument("--reference", type=Path, required=True)
    parser.add_argument("--report", type=Path, required=True)
    parser.add_argument("--summary", type=Path, required=True)
    args = parser.parse_args()
    result = score(args.submission, args.reference, args.report)
    write_diagnostic_report(args.summary, result)
    print(json.dumps(result, indent=2, sort_keys=True))
    return 0 if result["passed"] else 1


if __name__ == "__main__":
    raise SystemExit(main())
