"""Validate actual TRUSS v4 FreeCAD and manufacturing artifacts."""
import glob
import hashlib
import json
import os

import FreeCAD as App
import Mesh

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "out_v4")
FCSTD = os.path.join(OUT, "TRUSS_dual_mid360_scanner_v4.0.0.FCStd")
STEP = os.path.join(OUT, "TRUSS_dual_mid360_scanner_v4.0.0.step")
ASSEMBLY = os.path.join(OUT, "TRUSS_dual_mid360_scanner_v4.0.0.stl")
VERSION = "4.0.0-TRUSS"

checks = []
def check(name, passed, actual):
    checks.append({"name": name, "pass": bool(passed), "actual": actual})


for path in (FCSTD, STEP, ASSEMBLY):
    check("file exists: " + os.path.basename(path), os.path.exists(path) and os.path.getsize(path) > 10000,
          os.path.getsize(path) if os.path.exists(path) else None)

doc = App.openDocument(FCSTD)
main = doc.getObject("Integral_TRUSS_Chassis")
carrier = doc.getObject("Adjustable_Screen_Carrier")
feet = [doc.getObject("Threaded_Foot_%d" % i) for i in range(1, 5)]
lidars = [doc.getObject("Livox_MID360_LEFT"), doc.getObject("Livox_MID360_RIGHT")]
jackets = [doc.getObject("MID360_Cable_Jacket_LEFT"), doc.getObject("MID360_Cable_Jacket_RIGHT")]

engine = ".".join(map(str, App.Version()[:3]))
check("FreeCAD engine 1.1.3", engine == "1.1.3", engine)
check("integral chassis exists", main is not None, getattr(main, "Name", None))
check("integral chassis one B-rep solid", main is not None and len(main.Shape.Solids) == 1,
      len(main.Shape.Solids) if main else None)
check("integral chassis B-rep valid", main is not None and main.Shape.isValid(),
      main.Shape.isValid() if main else None)
check("thin wall declared 3 mm", main is not None and abs(main.NominalWallThickness.Value-3.0) < 1e-7,
      main.NominalWallThickness.Value if main else None)
check("circular cable guards declared", main is not None and "OD42/ID32" in main.CableGuards,
      getattr(main, "CableGuards", None))
check("direct LiDAR holes declared", main is not None and "M3 normal d3.4" in main.DirectMountHoles,
      getattr(main, "DirectMountHoles", None))

check("screen carrier exists", carrier is not None, getattr(carrier, "Name", None))
check("screen carrier one valid solid", carrier is not None and carrier.Shape.isValid() and len(carrier.Shape.Solids) == 1,
      {"valid": carrier.Shape.isValid(), "solids": len(carrier.Shape.Solids)} if carrier else None)
check("screen uses real revolute axis", carrier is not None and "global X" in carrier.Joint,
      getattr(carrier, "Joint", None))
check("screen range declared 10 to 45", carrier is not None and "10 to 45" in carrier.AdjustmentRange,
      getattr(carrier, "AdjustmentRange", None))

hinge_names = ["M6_Shoulder_Pin", "Belleville_Washer_LEFT", "Belleville_Washer_RIGHT", "M6_Nyloc_Nut"]
for name in hinge_names:
    obj = doc.getObject(name)
    check("real hinge component: " + name, obj is not None and not obj.Shape.isNull(),
          {"exists": obj is not None, "volume_mm3": round(obj.Shape.Volume, 3) if obj else None})

check("four foot instances", all(feet), [getattr(f, "Name", None) for f in feet])
check("one repeated foot part number", len(set(f.PartNumber for f in feet)) == 1,
      sorted(set(f.PartNumber for f in feet)))
for foot in feet:
    check(foot.Name + " one valid solid", foot.Shape.isValid() and len(foot.Shape.Solids) == 1,
          {"valid": foot.Shape.isValid(), "solids": len(foot.Shape.Solids), "edges": len(foot.Shape.Edges)})
    check(foot.Name + " modeled M12x1.75 helix", "fully modeled M12x1.75" in foot.ExternalThread and len(foot.Shape.Edges) > 40,
          {"property": foot.ExternalThread, "edges": len(foot.Shape.Edges)})

for lidar in lidars:
    check(lidar.Name + " official 7-solid assembly", len(lidar.Shape.Solids) == 7, len(lidar.Shape.Solids))
    check(lidar.Name + " corrected bottom mount", "+Y optical corrected to +Z" in lidar.OrientationProof,
          lidar.OrientationProof)
    check(lidar.Name + " plate angle 60 deg", abs(lidar.MountingPlaneFromGround.Value-60.0) < 1e-7,
          lidar.MountingPlaneFromGround.Value)
    check(lidar.Name + " connector outboard", "outboard" in lidar.ConnectorDirection,
          lidar.ConnectorDirection)

with open(os.path.join(OUT, "design-manifest.json"), encoding="utf-8") as handle:
    manifest = json.load(handle)
check("axis separation 120 deg", manifest["mid360_mount"]["axis_separation_deg"] == 120.0,
      manifest["mid360_mount"]["axis_separation_deg"])
check("circular plate area above Livox minimum", manifest["mid360_mount"]["plate_area_mm2"] >= 10000,
      manifest["mid360_mount"]["plate_area_mm2"])
check("official LiDAR source volume preserved", abs(manifest["mid360_orientation"]["volume_mm3"]-141854.316) < 0.01,
      manifest["mid360_orientation"]["volume_mm3"])
check("coverage used 240000 real calculations", manifest["coverage"]["samples"] == 240000,
      manifest["coverage"])

with open(os.path.join(OUT, "interference-report.json"), encoding="utf-8") as handle:
    interference = json.load(handle)
failures = [row for row in interference["checks"] if not row["pass"]]
max_common = max(row["common_volume_mm3"] for row in interference["checks"])
check("50-pair exact interference audit", len(interference["checks"]) == 50, len(interference["checks"]))
check("zero positive common volume", not failures and max_common == 0.0,
      {"failures": failures, "max_common_volume_mm3": max_common})
check("hinge motion checked at 10/20/45 deg", [r["pose_deg"] for r in interference["hinge_motion"]] == [10.0, 20.0, 45.0],
      interference["hinge_motion"])
check("hinge motion is collision free", all(r["pass"] for r in interference["hinge_motion"]),
      interference["hinge_motion"])

part_files = sorted(glob.glob(os.path.join(OUT, "parts", "*.stl")))
check("exactly three unique fabrication STL files", len(part_files) == 3,
      [os.path.basename(p) for p in part_files])
part_stats = []
for path in part_files:
    mesh = Mesh.Mesh(path)
    row = {"file": os.path.basename(path), "facets": mesh.CountFacets,
           "bbox_mm": [round(mesh.BoundBox.XLength, 3), round(mesh.BoundBox.YLength, 3),
                       round(mesh.BoundBox.ZLength, 3)]}
    part_stats.append(row)
    check("readable manufacturing STL: " + row["file"], mesh.CountFacets > 100, row)

assembly_mesh = Mesh.Mesh(ASSEMBLY)
check("assembly STL ground contact", abs(assembly_mesh.BoundBox.ZMin) < 0.001,
      assembly_mesh.BoundBox.ZMin)
check("assembly STL production detail", assembly_mesh.CountFacets > 100000,
      assembly_mesh.CountFacets)

hash_targets = [FCSTD, STEP, ASSEMBLY, os.path.join(OUT, "design-manifest.json"),
                os.path.join(OUT, "interference-report.json")] + part_files
hashes = {}
for path in hash_targets:
    with open(path, "rb") as handle:
        hashes[os.path.relpath(path, ROOT).replace("\\", "/")] = hashlib.sha256(handle.read()).hexdigest()

errors = [row for row in checks if not row["pass"]]
result = {
    "version": VERSION,
    "freecad_version": engine,
    "source_machine": os.environ.get("COMPUTERNAME", "unknown"),
    "real_artifact_counts": {
        "integral_chassis_solids": len(main.Shape.Solids),
        "screen_carrier_solids": len(carrier.Shape.Solids),
        "identical_foot_instances": len(feet),
        "official_livox_solids_each": [len(l.Shape.Solids) for l in lidars],
        "interference_pairs": len(interference["checks"]),
        "hinge_motion_poses": len(interference["hinge_motion"]),
        "assembly_stl_facets": assembly_mesh.CountFacets,
    },
    "part_stats": part_stats,
    "checks": checks,
    "errors": errors,
    "status": "PASS" if not errors else "FAIL",
    "sha256": hashes,
}
with open(os.path.join(OUT, "verification.json"), "w", encoding="utf-8") as handle:
    json.dump(result, handle, indent=2, ensure_ascii=False)
with open(os.path.join(OUT, "SHA256SUMS.txt"), "w", encoding="ascii") as handle:
    for path, digest in hashes.items():
        handle.write("%s  %s\n" % (digest, path))

print(json.dumps({"status": result["status"], "checks": len(checks), "errors": len(errors),
                  "counts": result["real_artifact_counts"], "sha256": hashes}, indent=2), flush=True)
if errors:
    raise RuntimeError("validation failed: %s" % errors)
