"""VINC AutoCAD Bridge for VINC MCP Agent.

Dual-engine CAD controller:
1. Live AutoCAD COM Automation (AutoCAD 2020-2026 on Windows via pywin32)
2. Headless DXF Engine fallback (via ezdxf)
"""

import sys
import os
import json
import math
import traceback

def get_acad_active():
    """Try to connect to active running AutoCAD instance."""
    try:
        import win32com.client
        import pythoncom
        pythoncom.CoInitialize()
        acad = win32com.client.GetActiveObject("AutoCAD.Application")
        return acad, acad.ActiveDocument
    except Exception as e:
        return None, None

def get_cad_status(params):
    """Check AutoCAD status on current machine."""
    acad, doc = get_acad_active()
    if acad and doc:
        try:
            layers = [doc.Layers.Item(i).Name for i in range(doc.Layers.Count)]
        except Exception:
            layers = []
        return {
            "mode": "live_autocad",
            "autocad_running": True,
            "version": getattr(acad, "Version", "Unknown"),
            "caption": getattr(acad, "Caption", "AutoCAD"),
            "active_document": getattr(doc, "Name", "Untitled"),
            "path": getattr(doc, "Path", ""),
            "modelspace_entities": getattr(doc.ModelSpace, "Count", 0),
            "layers_count": len(layers),
            "layers": layers[:25]
        }
    else:
        # Check ezdxf fallback
        try:
            import ezdxf
            ez_ver = ezdxf.__version__
        except Exception:
            ez_ver = None
        return {
            "mode": "headless_ezdxf",
            "autocad_running": False,
            "ezdxf_available": ez_ver is not None,
            "ezdxf_version": ez_ver,
            "note": "AutoCAD is not actively open. Commands can create DXF files headlessly or launch AutoCAD."
        }

def to_variant_point(coords):
    """Convert [x, y] or [x, y, z] to win32com VARIANT array of doubles."""
    import win32com.client
    x = float(coords[0]) if len(coords) > 0 else 0.0
    y = float(coords[1]) if len(coords) > 1 else 0.0
    z = float(coords[2]) if len(coords) > 2 else 0.0
    return win32com.client.VARIANT(win32com.client.VT_ARRAY | win32com.client.VT_R8, [x, y, z])

def to_variant_points_2d(coords_list):
    """Convert list of [x, y] to flat VARIANT array of doubles for AddLightWeightPolyline."""
    import win32com.client
    flat = []
    for pt in coords_list:
        flat.append(float(pt[0]))
        flat.append(float(pt[1]))
    return win32com.client.VARIANT(win32com.client.VT_ARRAY | win32com.client.VT_R8, flat)

def ensure_layer(doc, layer_name, color=None):
    """Ensure a layer exists and optionally set color."""
    if not layer_name:
        return
    try:
        layer = doc.Layers.Item(layer_name)
    except Exception:
        layer = doc.Layers.Add(layer_name)
    if color is not None:
        try:
            layer.color = int(color)
        except Exception:
            pass
    return layer

def draw_line(doc, params):
    start = params.get("start", [0, 0, 0])
    end = params.get("end", [100, 100, 0])
    layer = params.get("layer")
    color = params.get("color")

    if layer:
        ensure_layer(doc, layer, color)

    p1 = to_variant_point(start)
    p2 = to_variant_point(end)
    line = doc.ModelSpace.AddLine(p1, p2)
    if layer:
        line.Layer = str(layer)
    if color is not None:
        line.Color = int(color)
    return {"handle": getattr(line, "Handle", ""), "type": "LINE", "start": start, "end": end}

def draw_circle(doc, params):
    center = params.get("center", [0, 0, 0])
    radius = float(params.get("radius", 50.0))
    layer = params.get("layer")
    color = params.get("color")

    if layer:
        ensure_layer(doc, layer, color)

    cp = to_variant_point(center)
    circle = doc.ModelSpace.AddCircle(cp, radius)
    if layer:
        circle.Layer = str(layer)
    if color is not None:
        circle.Color = int(color)
    return {"handle": getattr(circle, "Handle", ""), "type": "CIRCLE", "center": center, "radius": radius}

def draw_arc(doc, params):
    center = params.get("center", [0, 0, 0])
    radius = float(params.get("radius", 50.0))
    start_angle = math.radians(float(params.get("start_angle", 0.0)))
    end_angle = math.radians(float(params.get("end_angle", 90.0)))
    layer = params.get("layer")

    if layer:
        ensure_layer(doc, layer)

    cp = to_variant_point(center)
    arc = doc.ModelSpace.AddArc(cp, radius, start_angle, end_angle)
    if layer:
        arc.Layer = str(layer)
    return {"handle": getattr(arc, "Handle", ""), "type": "ARC", "center": center, "radius": radius}

def draw_polyline(doc, params):
    points = params.get("points", [])
    closed = bool(params.get("closed", False))
    layer = params.get("layer")
    color = params.get("color")

    if len(points) < 2:
        raise ValueError("Polyline requires at least 2 points.")

    if layer:
        ensure_layer(doc, layer, color)

    pts_var = to_variant_points_2d(points)
    pline = doc.ModelSpace.AddLightWeightPolyline(pts_var)
    if closed:
        pline.Closed = True
    if layer:
        pline.Layer = str(layer)
    if color is not None:
        pline.Color = int(color)
    return {"handle": getattr(pline, "Handle", ""), "type": "LWPOLYLINE", "points_count": len(points), "closed": closed}

def draw_rectangle(doc, params):
    c1 = params.get("corner1", [0, 0])
    c2 = params.get("corner2", [100, 100])
    layer = params.get("layer")
    color = params.get("color")

    x1, y1 = float(c1[0]), float(c1[1])
    x2, y2 = float(c2[0]), float(c2[1])

    points = [
        [x1, y1],
        [x2, y1],
        [x2, y2],
        [x1, y2]
    ]
    return draw_polyline(doc, {"points": points, "closed": True, "layer": layer, "color": color})

def create_text(doc, params):
    text_str = str(params.get("text", ""))
    insertion = params.get("insertion", [0, 0, 0])
    height = float(params.get("height", 250.0))
    layer = params.get("layer")
    rotation = math.radians(float(params.get("rotation", 0.0)))
    is_mtext = bool(params.get("is_mtext", False))
    width = float(params.get("width", 2000.0))

    if layer:
        ensure_layer(doc, layer)

    ins_pt = to_variant_point(insertion)
    if is_mtext:
        ent = doc.ModelSpace.AddMText(ins_pt, width, text_str)
        ent.Height = height
    else:
        ent = doc.ModelSpace.AddText(text_str, ins_pt, height)
        if rotation != 0.0:
            ent.Rotation = rotation

    if layer:
        ent.Layer = str(layer)
    return {"handle": getattr(ent, "Handle", ""), "type": "MTEXT" if is_mtext else "TEXT", "text": text_str}

def create_layer(doc, params):
    name = str(params.get("name", "NEW_LAYER"))
    color = params.get("color")
    linetype = params.get("linetype")
    lineweight = params.get("lineweight")

    layer = ensure_layer(doc, name, color)
    if linetype:
        try:
            layer.Linetype = str(linetype)
        except Exception:
            pass
    if lineweight is not None:
        try:
            layer.Lineweight = int(lineweight)
        except Exception:
            pass
    return {"name": name, "color": color, "linetype": linetype}

def create_dimension(doc, params):
    p1 = params.get("ext_line_1", [0, 0, 0])
    p2 = params.get("ext_line_2", [100, 0, 0])
    text_line = params.get("text_position", [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2 + 50, 0])
    layer = params.get("layer", "0_DIM")

    ensure_layer(doc, layer, 3) # Green

    v1 = to_variant_point(p1)
    v2 = to_variant_point(p2)
    vt = to_variant_point(text_line)

    dim = doc.ModelSpace.AddDimAligned(v1, v2, vt)
    dim.Layer = layer
    return {"handle": getattr(dim, "Handle", ""), "type": "DIMENSION_ALIGNED"}

def run_command(doc, params):
    cmd = str(params.get("command", "")).strip()
    if not cmd:
        raise ValueError("Empty command string.")
    doc.SendCommand(cmd + "\n")
    return {"command": cmd, "status": "sent_to_autocad"}

def run_lisp(doc, params):
    code = str(params.get("code", "")).strip()
    if not code:
        raise ValueError("Empty LISP code.")
    # Send LISP directly to command line
    doc.SendCommand(code + "\n")
    return {"code": code, "status": "executed"}

def zoom_extents(doc, params):
    doc.SendCommand("_ZOOM _E\n")
    return {"status": "zoom_extents_executed"}

def batch_draw(doc, params):
    actions = params.get("actions", [])
    results = []
    for act in actions:
        op = act.get("op")
        p = act.get("params", {})
        if op == "line":
            results.append(draw_line(doc, p))
        elif op == "circle":
            results.append(draw_circle(doc, p))
        elif op == "arc":
            results.append(draw_arc(doc, p))
        elif op == "polyline":
            results.append(draw_polyline(doc, p))
        elif op == "rectangle":
            results.append(draw_rectangle(doc, p))
        elif op == "text":
            results.append(create_text(doc, p))
        elif op == "layer":
            results.append(create_layer(doc, p))
        elif op == "dimension":
            results.append(create_dimension(doc, p))
        elif op == "command":
            results.append(run_command(doc, p))
        elif op == "lisp":
            results.append(run_lisp(doc, p))
    # After batch, zoom extents if requested
    if params.get("zoom_extents", True):
        doc.SendCommand("_ZOOM _E\n")
    return {"executed_count": len(results), "results": results}

def get_entities(doc, params):
    msp = doc.ModelSpace
    count = msp.Count
    max_items = min(int(params.get("max_items", 100)), count)
    entities = []
    for i in range(max_items):
        try:
            ent = msp.Item(i)
            entities.append({
                "index": i,
                "handle": getattr(ent, "Handle", ""),
                "type": getattr(ent, "ObjectName", "AcDbEntity").replace("AcDb", "").upper(),
                "layer": getattr(ent, "Layer", ""),
                "color": getattr(ent, "Color", 256)
            })
        except Exception:
            continue
    return {"total_in_modelspace": count, "retrieved": len(entities), "entities": entities}

def save_drawing(doc, params):
    filepath = params.get("path")
    if filepath:
        doc.SaveAs(filepath)
        return {"status": "saved_as", "path": filepath}
    else:
        doc.Save()
        return {"status": "saved", "path": doc.FullName}

def export_dxf(doc, params):
    filepath = params.get("path")
    if not filepath:
        filepath = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"autocad_export_{doc.Name}.dxf")
    doc.SaveAs(filepath, 1) # 1 = DXF R12 / dxf format in COM
    return {"status": "exported_dxf", "path": filepath}

def capture_screenshot(doc, params):
    """Capture screenshot of the active AutoCAD window."""
    try:
        import win32gui
        import win32ui
        import win32con
        from PIL import Image
        import base64
        import io

        hwnd = win32gui.FindWindow("AutoCAD", None)
        if not hwnd:
            # Fallback search
            def enum_cb(h, acc):
                if "AutoCAD" in win32gui.GetWindowText(h):
                    acc.append(h)
                return True
            acc = []
            win32gui.EnumWindows(enum_cb, acc)
            if acc:
                hwnd = acc[0]

        if not hwnd:
            return {"error": "AutoCAD window not found for screenshot."}

        # Bring window to top and get rect
        win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
        win32gui.SetForegroundWindow(hwnd)
        left, top, right, bot = win32gui.GetWindowRect(hwnd)
        w = right - left
        h = bot - top

        hwndDC = win32gui.GetWindowDC(hwnd)
        mfcDC = win32ui.CreateDCFromHandle(hwndDC)
        saveDC = mfcDC.CreateCompatibleDC()
        saveBitMap = win32ui.CreateBitmap()
        saveBitMap.CreateCompatibleBitmap(mfcDC, w, h)
        saveDC.SelectObject(saveBitMap)
        saveDC.BitBlt((0, 0), (w, h), mfcDC, (0, 0), win32con.SRCCOPY)

        bmpinfo = saveBitMap.GetInfo()
        bmpstr = saveBitMap.GetBitmapBits(True)
        im = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']), bmpstr, 'raw', 'BGRX', 0, 1)

        win32gui.DeleteObject(saveBitMap.GetHandle())
        saveDC.DeleteDC()
        mfcDC.DeleteDC()
        win32gui.ReleaseDC(hwnd, hwndDC)

        # Convert to base64
        buffered = io.BytesIO()
        im.save(buffered, format="JPEG", quality=85)
        img_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
        return {"width": w, "height": h, "image_base64_jpeg": img_b64}
    except Exception as e:
        return {"error": f"Screenshot capture failed: {str(e)}"}

# Headless DXF handlers when AutoCAD is not open
def headless_create_dxf(params):
    import ezdxf
    output_path = params.get("path") or os.path.join(os.environ.get("TEMP", "C:\\Temp"), "vinc_headless.dxf")
    doc = ezdxf.new("R2018", setup=True)
    msp = doc.modelspace()
    actions = params.get("actions", [])
    for act in actions:
        op = act.get("op")
        p = act.get("params", {})
        layer = p.get("layer", "0")
        if layer not in doc.layers:
            doc.layers.add(layer)
        if op == "line":
            msp.add_line(p.get("start", [0, 0]), p.get("end", [100, 100]), dxfattribs={"layer": layer})
        elif op == "circle":
            msp.add_circle(p.get("center", [0, 0]), p.get("radius", 50), dxfattribs={"layer": layer})
        elif op == "text":
            msp.add_text(str(p.get("text", "")), dxfattribs={"layer": layer, "height": float(p.get("height", 250))}).set_placement(p.get("insertion", [0, 0]))
        elif op == "polyline" or op == "rectangle":
            pts = p.get("points", [])
            if op == "rectangle":
                c1, c2 = p.get("corner1", [0, 0]), p.get("corner2", [100, 100])
                pts = [[c1[0], c1[1]], [c2[0], c1[1]], [c2[0], c2[1]], [c1[0], c2[1]]]
            msp.add_lwpolyline(pts, close=bool(p.get("closed", True)), dxfattribs={"layer": layer})
    doc.saveas(output_path)
    return {"mode": "headless_ezdxf", "status": "dxf_created", "path": output_path, "actions_count": len(actions)}

def main():
    if len(sys.argv) < 2:
        print(json.dumps({"error": "No action specified."}))
        sys.exit(1)

    action = sys.argv[1]
    params = {}
    if len(sys.argv) >= 3 and sys.argv[2].strip():
        try:
            params = json.loads(sys.argv[2])
        except Exception as e:
            params = {"raw": sys.argv[2]}
    else:
        # Check if stdin has data without blocking
        try:
            import select
            if hasattr(select, "select"):
                r, _, _ = select.select([sys.stdin], [], [], 0.05)
                if r:
                    raw_input = sys.stdin.read()
                    if raw_input.strip():
                        params = json.loads(raw_input)
        except Exception:
            pass

    if action == "status":
        print(json.dumps(get_cad_status(params)))
        return

    acad, doc = get_acad_active()
    if not doc:
        # Fallback to headless DXF for drawing operations
        if action in ("batch", "headless_draw", "draw_line", "draw_circle", "draw_rectangle"):
            try:
                actions = [ {"op": action.replace("draw_", ""), "params": params} ] if action != "batch" else params.get("actions", [])
                res = headless_create_dxf({"actions": actions, "path": params.get("path")})
                print(json.dumps(res))
                return
            except Exception as e:
                print(json.dumps({"error": f"AutoCAD is not running and headless DXF failed: {str(e)}"}))
                sys.exit(1)
        print(json.dumps({"error": "AutoCAD is not currently running. Please launch AutoCAD or specify headless mode."}))
        sys.exit(1)

    try:
        if action == "line":
            res = draw_line(doc, params)
        elif action == "circle":
            res = draw_circle(doc, params)
        elif action == "arc":
            res = draw_arc(doc, params)
        elif action == "polyline":
            res = draw_polyline(doc, params)
        elif action == "rectangle":
            res = draw_rectangle(doc, params)
        elif action == "text":
            res = create_text(doc, params)
        elif action == "layer":
            res = create_layer(doc, params)
        elif action == "dimension":
            res = create_dimension(doc, params)
        elif action == "command":
            res = run_command(doc, params)
        elif action == "lisp":
            res = run_lisp(doc, params)
        elif action == "zoom_extents":
            res = zoom_extents(doc, params)
        elif action == "batch":
            res = batch_draw(doc, params)
        elif action == "get_entities":
            res = get_entities(doc, params)
        elif action == "save":
            res = save_drawing(doc, params)
        elif action == "export_dxf":
            res = export_dxf(doc, params)
        elif action == "screenshot":
            res = capture_screenshot(doc, params)
        else:
            res = {"error": f"Unknown action: {action}"}
        print(json.dumps(res))
    except Exception as e:
        print(json.dumps({"error": str(e), "traceback": traceback.format_exc()}))
        sys.exit(1)

if __name__ == "__main__":
    main()
