"""
query_engine.py — Natural language geometric query handler.

Supports both single-query (--query "...") and interactive REPL (--repl).
REPL keeps the model in memory between queries for speed.
All output is formatted ASCII tables.

Supported query types (see SKILL.md for full reference):
  bounding box          overall model extents
  face count            total faces by type
  all parts             full assembly listing
  list all holes        all cylindrical through-features
  list all mounting holes   cylinders dia < 15mm, axis ⊥ primary face
  holes diameter N      filter by diameter
  wall thickness        min distance between opposing parallel faces
  largest face          largest planar face area
  help                  list supported queries
  exit / quit           exit REPL
"""
import logging
import re
import textwrap
from typing import Optional

from .loader import StepModel

logger = logging.getLogger("step_processor.query")

# Regex patterns for query routing
_BBOX_RE      = re.compile(r"\b(bounding.?box|extents|dimensions|overall.?size)\b", re.I)
_FACECOUNT_RE = re.compile(r"\b(face.?count|how many faces|number of faces)\b", re.I)
_PARTS_RE     = re.compile(r"\ball.?parts|part.?list|assembly|components\b", re.I)
_HOLES_RE     = re.compile(r"\bholes?\b", re.I)
_MOUNTING_RE  = re.compile(r"\bmounting\b", re.I)
_DIA_RE       = re.compile(r"diameter\s+([\d.]+)\s*mm?", re.I)
_WALL_RE      = re.compile(r"\bwall.?thickness\b", re.I)
_LARGEST_RE   = re.compile(r"\blargest.?face\b", re.I)
_HELP_RE      = re.compile(r"\bhelp\b", re.I)
_EXIT_RE      = re.compile(r"\b(exit|quit|q)\b", re.I)


def run_query(model: StepModel, query: str) -> str:
    """Dispatch a query string and return formatted output."""
    q = query.strip()
    if _EXIT_RE.search(q):
        return "EXIT"
    if _HELP_RE.search(q):
        return _help_text()
    if _BBOX_RE.search(q):
        return _query_bounding_box(model)
    if _FACECOUNT_RE.search(q):
        return _query_face_count(model)
    if _PARTS_RE.search(q):
        return _query_all_parts(model)
    if _HOLES_RE.search(q):
        dia_match = _DIA_RE.search(q)
        dia_filter = float(dia_match.group(1)) if dia_match else None
        mounting_only = bool(_MOUNTING_RE.search(q))
        return _query_holes(model, mounting_only=mounting_only, dia_filter=dia_filter)
    if _WALL_RE.search(q):
        return _query_wall_thickness(model)
    if _LARGEST_RE.search(q):
        return _query_largest_face(model)
    return (f"Query not recognized: '{q}'\n"
            f"Type 'help' to see supported queries.")


def repl(model: StepModel, step_path):
    """Launch interactive REPL. Returns when user types exit/quit."""
    print(f"\nSTEP Query REPL — {step_path.name}")
    print("Type 'help' for supported queries, 'exit' to quit.\n")
    while True:
        try:
            q = input("> ").strip()
        except (EOFError, KeyboardInterrupt):
            print()
            break
        if not q:
            continue
        result = run_query(model, q)
        if result == "EXIT":
            break
        print(result)
        print()


# ── Query implementations ─────────────────────────────────────────────────────

def _query_bounding_box(model: StepModel) -> str:
    try:
        if model.backend == "build123d":
            bb = model.shape.bounding_box()
            x = round(bb.size.X, 2)
            y = round(bb.size.Y, 2)
            z = round(bb.size.Z, 2)
        else:
            bb = model.shape.bounding_box
            x = round(bb.XMax - bb.XMin, 2)
            y = round(bb.YMax - bb.YMin, 2)
            z = round(bb.ZMax - bb.ZMin, 2)
        return _table(
            f"BOUNDING BOX — {model.path.name}",
            ["Axis", "Dimension"],
            [["Width (X)",  f"{x} mm ({x/25.4:.3f} in)"],
             ["Depth (Y)",  f"{y} mm ({y/25.4:.3f} in)"],
             ["Height (Z)", f"{z} mm ({z/25.4:.3f} in)"]]
        )
    except Exception as e:
        return f"Bounding box query failed: {e}"


def _query_face_count(model: StepModel) -> str:
    if model.backend != "build123d":
        return f"Face count query requires build123d (loaded via {model.backend})"
    try:
        from build123d import Compound
        all_faces = model.shape.faces()
        planar = sum(1 for f in all_faces if f.geom_type() == "PLANE")
        cylindrical = sum(1 for f in all_faces if f.geom_type() == "CYLINDER")
        other = len(all_faces) - planar - cylindrical
        return _table(
            f"FACE COUNT — {model.path.name}",
            ["Type", "Count"],
            [["Planar", str(planar)],
             ["Cylindrical", str(cylindrical)],
             ["Other", str(other)],
             ["Total", str(len(all_faces))]]
        )
    except Exception as e:
        return f"Face count failed: {e}"


def _query_all_parts(model: StepModel) -> str:
    if not model.parts:
        return (f"No assembly structure found in {model.path.name}.\n"
                f"File appears to be a single solid body.")
    rows = []
    for p in model.parts:
        rows.append([
            p.get("part_number", "—"),
            p.get("name", "—"),
            str(p.get("quantity", 1)),
            str(p.get("level", 0)),
            p.get("parent", ""),
        ])
    return _table(
        f"ALL PARTS — {model.path.name}",
        ["#", "Name", "Qty", "Level", "Parent"],
        rows
    ) + f"\nTotal: {len(model.parts)} parts"


def _query_holes(model: StepModel, mounting_only: bool = False,
                 dia_filter: Optional[float] = None) -> str:
    if model.backend != "build123d":
        return f"Hole query requires build123d (loaded via {model.backend})"
    try:
        holes = _find_holes(model, mounting_only=mounting_only, dia_filter=dia_filter)
        if not holes:
            label = "mounting holes" if mounting_only else "holes"
            qualifier = f" ≈{dia_filter}mm" if dia_filter else ""
            return f"No {label}{qualifier} found in {model.path.name}."
        header = "MOUNTING HOLES" if mounting_only else "ALL HOLES"
        # Group by diameter bucket for summary view
        from collections import Counter
        dia_counts = Counter(round(h["dia"], 1) for h in holes)
        MAX_ROWS = 50
        display_holes = holes[:MAX_ROWS]
        rows = []
        for i, h in enumerate(display_holes, 1):
            rows.append([
                str(i),
                f"{h['dia']:.2f} mm",
                f"{h['depth']:.2f} mm" if h["depth"] else "—",
                f"({h['x']:.1f}, {h['y']:.1f}, {h['z']:.1f})",
            ])
        result = _table(
            f"{header} — {model.path.name}",
            ["#", "Diameter", "Depth", "Position (x,y,z)"],
            rows
        )
        result += f"\nShowing {len(display_holes)} of {len(holes)} unique hole locations"
        result += "\n\nDIAMETER SUMMARY"
        result += "\n" + "─" * 30
        for dia, count in sorted(dia_counts.items()):
            result += f"\n  {dia:.1f} mm  ×{count}"
        result += "\n" + "─" * 30
        return result
    except Exception as e:
        return f"Hole query failed: {e}"


def _find_holes(model: StepModel, mounting_only: bool, dia_filter):
    """Extract and deduplicate cylindrical faces from build123d model.

    Deduplication: round axis position to 1mm grid, group by (dia_bucket, x, y, z).
    This collapses multiple cylindrical faces from the same physical hole
    (e.g. inner + outer surface of same cylinder) into one entry.
    """
    from OCP.BRepAdaptor import BRepAdaptor_Surface
    from OCP.GeomAbs import GeomAbs_Cylinder

    seen = {}  # key → best entry
    try:
        faces = model.shape.faces()
    except Exception:
        return []

    for face in faces:
        try:
            adaptor = BRepAdaptor_Surface(face.wrapped)
            if adaptor.GetType() != GeomAbs_Cylinder:
                continue
            cyl = adaptor.Cylinder()
            r = cyl.Radius()
            dia = round(r * 2, 2)
            # Diameter filters
            if mounting_only and dia > 15.0:
                continue
            if dia_filter and abs(dia - dia_filter) > 0.5:
                continue
            axis_pt = cyl.Location()
            # Round to 1mm grid for deduplication
            gx = round(axis_pt.X())
            gy = round(axis_pt.Y())
            gz = round(axis_pt.Z())
            dia_bucket = round(dia, 1)
            key = (dia_bucket, gx, gy, gz)
            if key not in seen:
                bb = face.bounding_box()
                depth = round(max(bb.size.X, bb.size.Y, bb.size.Z), 2)
                seen[key] = {
                    "dia": dia,
                    "depth": depth,
                    "x": round(axis_pt.X(), 1),
                    "y": round(axis_pt.Y(), 1),
                    "z": round(axis_pt.Z(), 1),
                }
        except Exception:
            continue
    return list(seen.values())


def _query_wall_thickness(model: StepModel) -> str:
    if model.backend != "build123d":
        return f"Wall thickness query requires build123d (loaded via {model.backend})"
    try:
        faces = model.shape.faces()
        planar = [f for f in faces if f.geom_type() == "PLANE"]
        if len(planar) < 2:
            return "Insufficient planar faces to determine wall thickness."
        # Heuristic: find minimum non-zero distance between parallel opposing faces
        min_t = None
        for i, f1 in enumerate(planar):
            n1 = f1.normal_at()
            for f2 in planar[i+1:]:
                n2 = f2.normal_at()
                # Parallel if normals are anti-parallel
                dot = abs(n1.dot(n2))
                if dot > 0.99:
                    c1 = f1.center()
                    c2 = f2.center()
                    dist = round(abs((c1 - c2).dot(n1)), 3)
                    if dist > 0.01:
                        if min_t is None or dist < min_t:
                            min_t = dist
        if min_t is None:
            return "Could not determine wall thickness from available faces."
        return _table(
            f"WALL THICKNESS — {model.path.name}",
            ["Measurement", "Value"],
            [["Minimum wall thickness",
              f"{min_t} mm ({min_t/25.4:.3f} in)"]]
        )
    except Exception as e:
        return f"Wall thickness query failed: {e}"


def _query_largest_face(model: StepModel) -> str:
    if model.backend != "build123d":
        return f"Largest face query requires build123d (loaded via {model.backend})"
    try:
        faces = model.shape.faces()
        planar = [(f, f.area()) for f in faces if f.geom_type() == "PLANE"]
        if not planar:
            return "No planar faces found."
        largest, area = max(planar, key=lambda x: x[1])
        bb = largest.bounding_box()
        return _table(
            f"LARGEST PLANAR FACE — {model.path.name}",
            ["Property", "Value"],
            [["Area",    f"{round(area, 2)} mm²"],
             ["Width",   f"{round(bb.size.X, 2)} mm"],
             ["Height",  f"{round(bb.size.Z, 2)} mm"]]
        )
    except Exception as e:
        return f"Largest face query failed: {e}"


# ── Formatting helpers ─────────────────────────────────────────────────────────

def _table(title: str, headers: list, rows: list) -> str:
    col_widths = [len(h) for h in headers]
    for row in rows:
        for i, cell in enumerate(row):
            col_widths[i] = max(col_widths[i], len(str(cell)))
    sep = "─" * (sum(col_widths) + 3 * len(headers) - 1)
    lines = [title, sep]
    header_line = "  ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers))
    lines.append(header_line)
    lines.append(sep)
    for row in rows:
        lines.append("  ".join(str(cell).ljust(col_widths[i]) for i, cell in enumerate(row)))
    lines.append(sep)
    return "\n".join(lines)


def _help_text() -> str:
    return textwrap.dedent("""\
    SUPPORTED QUERIES
    ─────────────────────────────────────────────────────────────
    bounding box            Overall model extents (W×D×H in mm)
    face count              Faces by type (planar, cylindrical, other)
    all parts               Full assembly listing with quantities
    list all holes          All cylindrical through-features
    list all mounting holes Holes smaller than 15mm diameter
    holes diameter 4.2mm    Filter holes by specific diameter
    wall thickness          Minimum wall thickness estimate
    largest face            Largest planar face area
    help                    Show this message
    exit                    Exit the REPL
    ─────────────────────────────────────────────────────────────
    Tip: geometry queries require build123d backend.
    If the file loaded via FreeCAD fallback, only bounding box
    and parts list are available.""")
