import json
import os
import sys

import FreeCAD as App
import FreeCADGui as Gui
import Part
import Sketcher
from PySide import QtCore, QtWidgets


MARKER = "FREECAD_GUI_WORKFLOW_RESULT="
REQUESTED_STATE = os.environ.get("FREECAD_GUI_WORKFLOW_STATE", "success")


def progress(phase):
    print("FREECAD_GUI_WORKFLOW_PROGRESS=" + phase, file=sys.stderr, flush=True)


def emit(value, status=0):
    result_file = os.environ.get("FREECAD_GUI_WORKFLOW_RESULT_FILE", "")
    if result_file:
        parent = os.path.dirname(result_file)
        if parent:
            os.makedirs(parent, exist_ok=True)
        with open(result_file, "w", encoding="utf-8") as stream:
            json.dump(value, stream, sort_keys=True, separators=(",", ":"))
            stream.write("\n")
        os._exit(status)
    payload = (MARKER + json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
    offset = 0
    while offset < len(payload):
        offset += os.write(sys.stdout.fileno(), payload[offset:])
    os._exit(status)


def flush_gui():
    Gui.updateGui()
    QtWidgets.QApplication.processEvents()


def active_edit_name():
    gui_document = Gui.activeDocument()
    in_edit = gui_document.getInEdit() if gui_document else None
    obj = getattr(in_edit, "Object", None) if in_edit else None
    return str(getattr(obj, "Name", ""))


def task_panel_state(phase):
    main_window = Gui.getMainWindow()
    focus = QtWidgets.QApplication.focusWidget()
    buttons = []
    for box in main_window.findChildren(QtWidgets.QDialogButtonBox):
        if not box.isVisible():
            continue
        for button in box.buttons():
            buttons.append({
                "text": str(button.text()).replace("&", ""),
                "objectName": str(button.objectName()),
                "enabled": bool(button.isEnabled()),
                "default": bool(button.isDefault()),
                "role": str(box.buttonRole(button)),
            })
    known_button_ids = {id(button) for box in main_window.findChildren(QtWidgets.QDialogButtonBox) for button in box.buttons()}
    for button in main_window.findChildren(QtWidgets.QPushButton):
        if id(button) in known_button_ids or not button.isVisible():
            continue
        buttons.append({
            "text": str(button.text()).replace("&", ""),
            "objectName": str(button.objectName()),
            "enabled": bool(button.isEnabled()),
            "default": bool(button.isDefault()),
            "role": "direct-push-button",
        })
    fields = []
    field_types = (
        QtWidgets.QLineEdit,
        QtWidgets.QComboBox,
        QtWidgets.QSpinBox,
        QtWidgets.QDoubleSpinBox,
        QtWidgets.QCheckBox,
    )
    seen = set()
    for field_type in field_types:
        for widget in main_window.findChildren(field_type):
            identity = id(widget)
            if identity in seen or not widget.isVisible():
                continue
            seen.add(identity)
            fields.append({
                "className": str(widget.metaObject().className()),
                "objectName": str(widget.objectName()),
                "enabled": bool(widget.isEnabled()),
                "focus": widget is focus,
            })
    return {
        "phase": phase,
        "activeDialog": bool(Gui.Control.activeDialog()),
        "inEdit": active_edit_name(),
        "focus": None if focus is None else {
            "className": str(focus.metaObject().className()),
            "objectName": str(focus.objectName()),
        },
        "buttons": buttons,
        "fieldCount": len(fields),
        "fieldClasses": sorted({field["className"] for field in fields}),
    }


def click_accept():
    main_window = Gui.getMainWindow()
    def finish_click(button):
        button.click()
        flush_gui()
        if Gui.Control.activeDialog() or active_edit_name():
            Gui.activeDocument().resetEdit()
            flush_gui()
        return True

    candidates = []
    for box in main_window.findChildren(QtWidgets.QDialogButtonBox):
        if not box.isVisible():
            continue
        for button in box.buttons():
            if box.buttonRole(button) == QtWidgets.QDialogButtonBox.AcceptRole and button.isEnabled():
                candidates.append(button)
    if candidates:
        return finish_click(candidates[0])
    for button in main_window.findChildren(QtWidgets.QPushButton):
        text = str(button.text()).replace("&", "").strip().lower()
        if button.isVisible() and button.isEnabled() and text in ("ok", "accept", "done"):
            return finish_click(button)
    dialog = Gui.Control.activeDialog()
    if dialog and hasattr(dialog, "accept"):
        dialog.accept()
        flush_gui()
        return True
    return False


def selection_names():
    return [str(item.ObjectName) for item in Gui.Selection.getSelectionEx()]


def available_transactions(document, method_name):
    method = getattr(document, method_name, None)
    if not callable(method):
        return []
    try:
        return [str(value) for value in method()]
    except Exception:
        return []


def profile_name(feature):
    value = getattr(feature, "Profile", None)
    if isinstance(value, tuple) and value:
        return str(getattr(value[0], "Name", ""))
    return str(getattr(value, "Name", ""))


def counter(document, name):
    try:
        return int(getattr(document, name))
    except Exception:
        return None


def active_transaction_state():
    transaction = App.getActiveTransaction()
    return {
        "active": transaction is not None,
        "name": str(transaction[0]) if transaction else "",
        "id": int(transaction[1]) if transaction else None,
        "documents": [
            {
                "name": str(document.Name),
                "hasPendingTransaction": bool(document.HasPendingTransaction),
                "undoCount": counter(document, "UndoCount"),
                "redoCount": counter(document, "RedoCount"),
                "undoNames": available_transactions(document, "getAvailableUndoNames"),
                "redoNames": available_transactions(document, "getAvailableRedoNames"),
            }
            for document in App.listDocuments().values()
        ],
    }


def disabled_side_effect_state(phase):
    app_document = App.ActiveDocument
    gui_document = Gui.activeDocument()
    return {
        "phase": phase,
        "documents": sorted(str(name) for name in App.listDocuments()),
        "documentCount": len(App.listDocuments()),
        "activeDocument": str(app_document.Name) if app_document else "",
        "activeGuiDocument": gui_document is not None,
        "selection": selection_names(),
        "taskPanel": {
            "activeDialog": bool(Gui.Control.activeDialog()),
            "inEdit": active_edit_name(),
        },
        "transaction": active_transaction_state(),
    }


def focus_state():
    focus = QtWidgets.QApplication.focusWidget()
    return None if focus is None else {
        "className": str(focus.metaObject().className()),
        "objectName": str(focus.objectName()),
    }


def active_body_name():
    active_body = Gui.activeView().getActiveObject("pdbody") if Gui.activeDocument() else None
    return str(getattr(active_body, "Name", "")) if active_body else ""


def run_disabled_workflow():
    progress("activate-workbench:start")
    Gui.activateWorkbench("PartDesignWorkbench")
    flush_gui()
    progress("activate-workbench:done")
    Gui.Selection.clearSelection()
    flush_gui()

    command = Gui.Command.get("PartDesign_Pad")
    actions = list(command.getAction()) if command else []
    before_side_effects = disabled_side_effect_state("before")
    before = {
        "activeWorkbench": str(Gui.activeWorkbench().name()),
        "commandRegistered": command is not None,
        "commandActive": bool(command.isActive()) if command else False,
        "actionCount": len(actions),
        "actionEnabled": bool(actions[0].isEnabled()) if actions else None,
        "sideEffects": before_side_effects,
    }

    action_signal_count = [0]
    if actions:
        actions[0].triggered.connect(lambda _checked=False: action_signal_count.__setitem__(0, action_signal_count[0] + 1))
        progress("disabled-action-trigger:start")
        actions[0].trigger()
        flush_gui()
        progress("disabled-action-trigger:done")
    after_action_trigger = disabled_side_effect_state("after-action-trigger")

    progress("disabled-run-command:start")
    Gui.runCommand("PartDesign_Pad", 0)
    flush_gui()
    progress("disabled-run-command:done")
    after_run_command = disabled_side_effect_state("after-run-command")
    after = {
        "commandActive": bool(command.isActive()) if command else False,
        "actionEnabled": bool(actions[0].isEnabled()) if actions else None,
        "sideEffects": after_run_command,
    }

    def stable_side_effects(value):
        return {key: item for key, item in value.items() if key != "phase"}

    unchanged_after_action = stable_side_effects(after_action_trigger) == stable_side_effects(before_side_effects)
    unchanged_after_run_command = stable_side_effects(after_run_command) == stable_side_effects(before_side_effects)
    version = App.Version()
    result = {
        "schemaVersion": 1,
        "baselineId": "freecad-1.1.1",
        "freecadVersion": ".".join(str(value) for value in version[:3]),
        "gitCommit": str(version[7]) if len(version) > 7 else "",
        "workflowId": "partdesign-pad-task",
        "commandId": "PartDesign_Pad",
        "state": "disabled",
        "before": before,
        "activationAttempts": {
            "actionTrigger": {
                "requested": bool(actions),
                "triggeredSignalCount": action_signal_count[0],
                "sideEffectsUnchanged": unchanged_after_action,
                "after": after_action_trigger,
            },
            "runCommand": {
                "requested": True,
                "sideEffectsUnchanged": unchanged_after_run_command,
                "after": after_run_command,
            },
        },
        "after": after,
        "success": bool(
            command is not None
            and actions
            and not before["commandActive"]
            and before["actionEnabled"] is False
            and action_signal_count[0] == 0
            and unchanged_after_action
            and unchanged_after_run_command
            and after_run_command["documentCount"] == 0
            and not after_run_command["taskPanel"]["activeDialog"]
            and not after_run_command["taskPanel"]["inEdit"]
            and not after_run_command["transaction"]["active"]
            and not after_run_command["transaction"]["documents"]
        ),
    }
    progress("result:done")
    emit(result, 0 if result["success"] else 2)


def accept_task_capturing_warning():
    captured = []
    polls = [0]

    def capture_warning():
        modal = QtWidgets.QApplication.activeModalWidget()
        if isinstance(modal, QtWidgets.QMessageBox):
            captured.append({
                "title": str(modal.windowTitle()),
                "text": str(modal.text()),
                "informativeText": str(modal.informativeText()),
                "detailedText": str(modal.detailedText()),
                "standardButtons": int(modal.standardButtons()),
            })
            button = modal.button(QtWidgets.QMessageBox.Ok)
            if button:
                button.click()
            else:
                modal.accept()
            return
        polls[0] += 1
        if polls[0] < 200:
            QtCore.QTimer.singleShot(5, capture_warning)

    task = Gui.Control.activeTaskDialog()
    if task is None:
        return {"requested": False, "warnings": captured, "polls": polls[0]}
    QtCore.QTimer.singleShot(0, capture_warning)
    task.accept()
    flush_gui()
    return {"requested": True, "warnings": captured, "polls": polls[0]}


def run_failure_workflow():
    progress("activate-workbench:start")
    Gui.activateWorkbench("PartDesignWorkbench")
    flush_gui()
    progress("activate-workbench:done")
    document = App.newDocument("PadGuiFailure")
    document.UndoMode = 1
    body = document.addObject("PartDesign::Body", "Body")
    sketch = body.newObject("Sketcher::SketchObject", "Sketch")
    points = [App.Vector(0, 0, 0), App.Vector(4, 0, 0), App.Vector(4, 3, 0)]
    for index in range(2):
        sketch.addGeometry(Part.LineSegment(points[index], points[index + 1]), False)
    document.recompute()
    Gui.activeView().setActiveObject("pdbody", body)
    Gui.Selection.clearSelection()
    Gui.Selection.addSelection(document.Name, sketch.Name)
    flush_gui()

    baseline = {
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "selection": selection_names(),
        "sketchGeometryCount": int(sketch.GeometryCount),
        "sketchShapeValid": bool(sketch.Shape.isValid()),
        "sketchState": [str(value) for value in sketch.State],
        "transaction": active_transaction_state(),
    }
    command = Gui.Command.get("PartDesign_Pad")
    actions = list(command.getAction()) if command else []
    before = {
        "activeWorkbench": str(Gui.activeWorkbench().name()),
        "commandRegistered": command is not None,
        "commandActive": bool(command.isActive()) if command else False,
        "actionCount": len(actions),
        "actionEnabled": bool(actions[0].isEnabled()) if actions else None,
        "baseline": baseline,
    }

    progress("failure-run-command:start")
    Gui.runCommand("PartDesign_Pad", 0)
    flush_gui()
    progress("failure-run-command:done")
    pad = document.getObject("Pad")
    opened = task_panel_state("failure-opened")
    preview = {
        "exists": pad is not None,
        "typeId": str(pad.TypeId) if pad else "",
        "profile": profile_name(pad) if pad else "",
        "shapeNull": bool(pad.Shape.isNull()) if pad else None,
        "shapeValid": bool(pad.Shape.isValid()) if pad and not pad.Shape.isNull() else False if pad else None,
        "state": [str(value) for value in pad.State] if pad else [],
        "statusString": str(pad.getStatusString()) if pad else "",
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "transaction": active_transaction_state(),
    }

    progress("failure-accept:start")
    diagnostic = accept_task_capturing_warning()
    progress("failure-accept:done")
    pad = document.getObject("Pad")
    after_attempt = {
        "taskPanel": task_panel_state("failure-after-accept"),
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "padExists": pad is not None,
        "padShapeNull": bool(pad.Shape.isNull()) if pad else None,
        "padState": [str(value) for value in pad.State] if pad else [],
        "padStatusString": str(pad.getStatusString()) if pad else "",
        "transaction": active_transaction_state(),
    }

    progress("failure-cleanup:start")
    task = Gui.Control.activeTaskDialog()
    cleanup_requested = task is not None
    if task:
        task.reject()
    flush_gui()
    document.recompute()
    flush_gui()
    progress("failure-cleanup:done")
    after_cleanup = {
        "rejectRequested": cleanup_requested,
        "taskPanel": task_panel_state("failure-cleaned"),
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "padExists": document.getObject("Pad") is not None,
        "selection": selection_names(),
        "sketchGeometryCount": int(sketch.GeometryCount),
        "sketchShapeValid": bool(sketch.Shape.isValid()),
        "sketchState": [str(value) for value in sketch.State],
        "sketchVisible": bool(sketch.Visibility),
        "transaction": active_transaction_state(),
    }
    warning = diagnostic["warnings"][0] if diagnostic["warnings"] else None
    version = App.Version()
    result = {
        "schemaVersion": 1,
        "baselineId": "freecad-1.1.1",
        "freecadVersion": ".".join(str(value) for value in version[:3]),
        "gitCommit": str(version[7]) if len(version) > 7 else "",
        "workflowId": "partdesign-pad-task",
        "commandId": "PartDesign_Pad",
        "state": "failure",
        "before": before,
        "taskPanel": {"opened": opened, "preview": preview, "afterAttempt": after_attempt},
        "diagnostic": diagnostic,
        "after": after_cleanup,
        "success": bool(
            before["commandActive"]
            and opened["activeDialog"]
            and opened["inEdit"] == "Pad"
            and preview["exists"]
            and preview["shapeNull"]
            and "Invalid" in preview["state"]
            and diagnostic["requested"]
            and warning
            and warning["title"] == "Input error"
            and warning["text"] == preview["statusString"]
            and after_attempt["taskPanel"]["activeDialog"]
            and after_attempt["taskPanel"]["inEdit"] == "Pad"
            and after_attempt["padExists"]
            and after_attempt["padShapeNull"]
            and after_cleanup["rejectRequested"]
            and not after_cleanup["taskPanel"]["activeDialog"]
            and not after_cleanup["taskPanel"]["inEdit"]
            and not after_cleanup["padExists"]
            and after_cleanup["objectNames"] == baseline["objectNames"]
            and after_cleanup["bodyTip"] == baseline["bodyTip"]
            and after_cleanup["sketchGeometryCount"] == baseline["sketchGeometryCount"]
            and after_cleanup["sketchShapeValid"] == baseline["sketchShapeValid"]
            and not after_cleanup["transaction"]["active"]
        ),
    }
    progress("result:done")
    App.closeDocument(document.Name)
    emit(result, 0 if result["success"] else 2)


def run_cancel_workflow():
    progress("activate-workbench:start")
    Gui.activateWorkbench("PartDesignWorkbench")
    flush_gui()
    progress("activate-workbench:done")
    document = App.newDocument("PadGuiCancel")
    document.UndoMode = 1
    body = document.addObject("PartDesign::Body", "Body")
    sketch = body.newObject("Sketcher::SketchObject", "Sketch")
    points = [
        App.Vector(0, 0, 0),
        App.Vector(4, 0, 0),
        App.Vector(4, 3, 0),
        App.Vector(0, 3, 0),
    ]
    for index in range(4):
        sketch.addGeometry(Part.LineSegment(points[index], points[(index + 1) % 4]), False)
    document.recompute()
    Gui.activeView().setActiveObject("pdbody", body)
    Gui.Selection.clearSelection()
    Gui.Selection.addSelection(document.Name, sketch.Name)
    viewport_widgets = [
        widget
        for widget in Gui.getMainWindow().findChildren(QtWidgets.QWidget)
        if str(widget.metaObject().className()) == "Gui::View3DInventorViewer"
    ]
    if viewport_widgets:
        viewport_widgets[0].setFocus(QtCore.Qt.OtherFocusReason)
    flush_gui()

    baseline = {
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "activeBody": active_body_name(),
        "selection": selection_names(),
        "focus": focus_state(),
        "sketchGeometryCount": int(sketch.GeometryCount),
        "sketchShapeValid": bool(sketch.Shape.isValid()),
        "transaction": active_transaction_state(),
    }
    command = Gui.Command.get("PartDesign_Pad")
    actions = list(command.getAction()) if command else []
    before = {
        "activeWorkbench": str(Gui.activeWorkbench().name()),
        "commandRegistered": command is not None,
        "commandActive": bool(command.isActive()) if command else False,
        "actionCount": len(actions),
        "actionEnabled": bool(actions[0].isEnabled()) if actions else None,
        "baseline": baseline,
    }

    progress("cancel-run-command:start")
    Gui.runCommand("PartDesign_Pad", 0)
    flush_gui()
    progress("cancel-run-command:done")
    opened = task_panel_state("cancel-opened")
    pad = document.getObject("Pad")
    if pad:
        pad.Length = 17.5
        document.recompute()
        flush_gui()
    preview = {
        "exists": pad is not None,
        "typeId": str(pad.TypeId) if pad else "",
        "length": round(float(pad.Length.Value), 9) if pad else None,
        "shapeValid": bool(pad.Shape.isValid()) if pad else None,
        "solidCount": len(pad.Shape.Solids) if pad else None,
        "volume": round(float(pad.Shape.Volume), 9) if pad else None,
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "activeBody": active_body_name(),
        "selection": selection_names(),
        "focus": focus_state(),
        "transaction": active_transaction_state(),
    }

    progress("cancel-reject:start")
    task = Gui.Control.activeTaskDialog()
    reject_requested = task is not None
    if task:
        task.reject()
    flush_gui()
    document.recompute()
    flush_gui()
    progress("cancel-reject:done")
    after = {
        "rejectRequested": reject_requested,
        "taskPanel": task_panel_state("cancel-closed"),
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "activeBody": active_body_name(),
        "padExists": document.getObject("Pad") is not None,
        "selection": selection_names(),
        "focus": focus_state(),
        "sketchGeometryCount": int(sketch.GeometryCount),
        "sketchShapeValid": bool(sketch.Shape.isValid()),
        "sketchVisible": bool(sketch.Visibility),
        "transaction": active_transaction_state(),
    }
    version = App.Version()
    result = {
        "schemaVersion": 1,
        "baselineId": "freecad-1.1.1",
        "freecadVersion": ".".join(str(value) for value in version[:3]),
        "gitCommit": str(version[7]) if len(version) > 7 else "",
        "workflowId": "partdesign-pad-task",
        "commandId": "PartDesign_Pad",
        "state": "cancel",
        "before": before,
        "taskPanel": {"opened": opened, "preview": preview, "closed": after["taskPanel"]},
        "after": after,
        "nativeSelectionOutcome": "selected-profile-consumed",
        "nativeFocusOutcome": "task-view-after-cancel",
        "success": bool(
            before["commandActive"]
            and baseline["selection"] == ["Sketch"]
            and baseline["activeBody"] == "Body"
            and baseline["focus"] == {"className": "Gui::View3DInventorViewer", "objectName": ""}
            and opened["activeDialog"]
            and opened["inEdit"] == "Pad"
            and preview["exists"]
            and preview["typeId"] == "PartDesign::Pad"
            and preview["length"] == 17.5
            and preview["shapeValid"]
            and preview["solidCount"] == 1
            and abs(preview["volume"] - 210.0) < 1e-7
            and preview["selection"] == []
            and preview["activeBody"] == baseline["activeBody"]
            and preview["transaction"]["active"]
            and after["rejectRequested"]
            and not after["taskPanel"]["activeDialog"]
            and not after["taskPanel"]["inEdit"]
            and not after["padExists"]
            and after["objectNames"] == baseline["objectNames"]
            and after["bodyTip"] == baseline["bodyTip"]
            and after["activeBody"] == baseline["activeBody"]
            and after["selection"] == []
            and after["focus"] == {"className": "Gui::TaskView::TaskView", "objectName": "Tasks"}
            and after["sketchGeometryCount"] == baseline["sketchGeometryCount"]
            and after["sketchShapeValid"] == baseline["sketchShapeValid"]
            and after["sketchVisible"]
            and not after["transaction"]["active"]
        ),
    }
    progress("result:done")
    App.closeDocument(document.Name)
    emit(result, 0 if result["success"] else 2)


def recovery_document_state(document, phase):
    body = document.getObject("Body")
    sketch = document.getObject("Sketch")
    pad = document.getObject("Pad")
    return {
        "phase": phase,
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyGroup": [str(obj.Name) for obj in body.Group] if body else [],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body and body.Tip else "",
        "sketchGeometryCount": int(sketch.GeometryCount) if sketch else None,
        "sketchShapeValid": bool(sketch.Shape.isValid()) if sketch else None,
        "padExists": pad is not None,
        "padTypeId": str(pad.TypeId) if pad else "",
        "padLength": round(float(pad.Length.Value), 9) if pad else None,
        "padProfile": profile_name(pad) if pad else "",
        "padShapeValid": bool(pad.Shape.isValid()) if pad and not pad.Shape.isNull() else False if pad else None,
        "padSolidCount": len(pad.Shape.Solids) if pad and not pad.Shape.isNull() else 0 if pad else None,
        "padFaceCount": len(pad.Shape.Faces) if pad and not pad.Shape.isNull() else 0 if pad else None,
        "padEdgeCount": len(pad.Shape.Edges) if pad and not pad.Shape.isNull() else 0 if pad else None,
        "padVertexCount": len(pad.Shape.Vertexes) if pad and not pad.Shape.isNull() else 0 if pad else None,
        "padVolume": round(float(pad.Shape.Volume), 9) if pad and not pad.Shape.isNull() else 0 if pad else None,
        "padState": [str(value) for value in pad.State] if pad else [],
        "undoCount": counter(document, "UndoCount"),
        "redoCount": counter(document, "RedoCount"),
        "undoNames": available_transactions(document, "getAvailableUndoNames"),
        "redoNames": available_transactions(document, "getAvailableRedoNames"),
        "transaction": active_transaction_state(),
    }


def run_recovery_workflow():
    recovery_file = os.environ.get("FREECAD_GUI_WORKFLOW_RECOVERY_FILE", "")
    if not recovery_file:
        raise RuntimeError("FREECAD_GUI_WORKFLOW_RECOVERY_FILE is required")
    progress("activate-workbench:start")
    Gui.activateWorkbench("PartDesignWorkbench")
    flush_gui()
    progress("activate-workbench:done")
    document = App.newDocument("PadGuiRecovery")
    document.UndoMode = 1
    body = document.addObject("PartDesign::Body", "Body")
    sketch = body.newObject("Sketcher::SketchObject", "Sketch")
    points = [
        App.Vector(0, 0, 0),
        App.Vector(4, 0, 0),
        App.Vector(4, 3, 0),
        App.Vector(0, 3, 0),
    ]
    for index in range(2):
        sketch.addGeometry(Part.LineSegment(points[index], points[index + 1]), False)
    document.recompute()
    Gui.activeView().setActiveObject("pdbody", body)
    Gui.Selection.clearSelection()
    Gui.Selection.addSelection(document.Name, sketch.Name)
    flush_gui()
    baseline = recovery_document_state(document, "open-wire-baseline")

    progress("recovery-run-command:start")
    Gui.runCommand("PartDesign_Pad", 0)
    flush_gui()
    progress("recovery-run-command:done")
    initial_pad = document.getObject("Pad")
    initial_failure = {
        "taskPanel": task_panel_state("recovery-invalid-opened"),
        "padShapeNull": bool(initial_pad.Shape.isNull()) if initial_pad else None,
        "padState": [str(value) for value in initial_pad.State] if initial_pad else [],
        "padStatusString": str(initial_pad.getStatusString()) if initial_pad else "",
        "diagnostic": accept_task_capturing_warning(),
    }
    initial_failure["afterAttempt"] = task_panel_state("recovery-invalid-after-accept")

    progress("recovery-repair:start")
    sketch.addGeometry(Part.LineSegment(points[2], points[3]), False)
    sketch.addGeometry(Part.LineSegment(points[3], points[0]), False)
    document.recompute()
    pad = document.getObject("Pad")
    # FreeCAD exposes Gui::PrefQuantitySpinBox to PySide as a generic QWidget;
    # rawValue is its double Qt property, while value expects Base::Quantity.
    length_edit = Gui.getMainWindow().findChild(QtWidgets.QWidget, "lengthEdit")
    if length_edit:
        length_edit.setProperty("rawValue", 12.0)
        flush_gui()
    elif pad:
        pad.Length = 12.0
    document.recompute()
    flush_gui()
    repaired_preview = recovery_document_state(document, "repaired-preview")
    repaired_preview["lengthInputUpdated"] = bool(
        length_edit is not None and abs(float(length_edit.property("rawValue")) - 12.0) < 1e-9
    )
    repaired_preview["taskPanel"] = task_panel_state("recovery-repaired")
    progress("recovery-repair:done")

    progress("recovery-accept:start")
    task = Gui.Control.activeTaskDialog()
    accept_requested = task is not None
    if task:
        task.accept()
    flush_gui()
    document.recompute()
    flush_gui()
    progress("recovery-accept:done")
    committed = recovery_document_state(document, "committed")
    committed["acceptRequested"] = accept_requested
    committed["taskPanel"] = task_panel_state("recovery-committed")

    progress("recovery-undo:start")
    document.undo()
    document.recompute()
    flush_gui()
    undo = recovery_document_state(document, "undo")
    progress("recovery-undo:done")

    progress("recovery-redo:start")
    document.redo()
    document.recompute()
    flush_gui()
    redo = recovery_document_state(document, "redo")
    progress("recovery-redo:done")

    progress("recovery-save:start")
    document.saveAs(recovery_file)
    flush_gui()
    saved = recovery_document_state(document, "saved")
    saved["fileName"] = str(document.FileName)
    document_name = document.Name
    App.closeDocument(document_name)
    reopened_document = App.openDocument(recovery_file)
    reopened_document.recompute()
    flush_gui()
    reopened = recovery_document_state(reopened_document, "reopened")
    reopened["fileName"] = str(reopened_document.FileName)
    reopened_document.save()
    App.closeDocument(reopened_document.Name)
    resaved_document = App.openDocument(recovery_file)
    resaved_document.recompute()
    flush_gui()
    resaved = recovery_document_state(resaved_document, "resaved-reopened")
    resaved["fileName"] = str(resaved_document.FileName)
    progress("recovery-save:done")

    warning = initial_failure["diagnostic"]["warnings"][0] if initial_failure["diagnostic"]["warnings"] else None

    def valid_pad_state(value):
        return bool(
            value["padExists"]
            and value["bodyTip"] == "Pad"
            and value["bodyGroup"] == ["Sketch", "Pad"]
            and value["sketchGeometryCount"] == 4
            and value["padTypeId"] == "PartDesign::Pad"
            and value["padLength"] == 12
            and value["padProfile"] == "Sketch"
            and value["padShapeValid"]
            and value["padSolidCount"] == 1
            and value["padFaceCount"] == 6
            and value["padEdgeCount"] == 12
            and value["padVertexCount"] == 8
            and abs(value["padVolume"] - 144.0) < 1e-7
            and value["padState"] == ["Up-to-date"]
            and not value["transaction"]["active"]
        )

    version = App.Version()
    result = {
        "schemaVersion": 1,
        "baselineId": "freecad-1.1.1",
        "freecadVersion": ".".join(str(value) for value in version[:3]),
        "gitCommit": str(version[7]) if len(version) > 7 else "",
        "workflowId": "partdesign-pad-task",
        "commandId": "PartDesign_Pad",
        "state": "recovery",
        "before": baseline,
        "initialFailure": initial_failure,
        "repairedPreview": repaired_preview,
        "committed": committed,
        "undo": undo,
        "redo": redo,
        "persistence": {"saved": saved, "reopened": reopened, "resavedReopened": resaved},
        "success": bool(
            baseline["sketchGeometryCount"] == 2
            and not baseline["padExists"]
            and initial_failure["taskPanel"]["activeDialog"]
            and initial_failure["taskPanel"]["inEdit"] == "Pad"
            and initial_failure["padShapeNull"]
            and "Invalid" in initial_failure["padState"]
            and warning
            and warning["title"] == "Input error"
            and warning["text"] == "Wire is not closed."
            and initial_failure["afterAttempt"]["activeDialog"]
            and initial_failure["afterAttempt"]["inEdit"] == "Pad"
            and repaired_preview["taskPanel"]["activeDialog"]
            and repaired_preview["taskPanel"]["inEdit"] == "Pad"
            and repaired_preview["lengthInputUpdated"]
            and repaired_preview["padShapeValid"]
            and repaired_preview["padVolume"] == 144
            and committed["acceptRequested"]
            and not committed["taskPanel"]["activeDialog"]
            and not committed["taskPanel"]["inEdit"]
            and valid_pad_state(committed)
            and not undo["padExists"]
            and undo["sketchGeometryCount"] == 2
            and undo["bodyTip"] == ""
            and undo["redoCount"] >= 1
            and valid_pad_state(redo)
            and redo["undoCount"] >= 1
            and valid_pad_state(saved)
            and saved["fileName"] == recovery_file
            and valid_pad_state(reopened)
            and reopened["fileName"] == recovery_file
            and valid_pad_state(resaved)
            and resaved["fileName"] == recovery_file
        ),
    }
    progress("result:done")
    App.closeDocument(resaved_document.Name)
    emit(result, 0 if result["success"] else 2)


try:
    if REQUESTED_STATE == "disabled":
        run_disabled_workflow()
    if REQUESTED_STATE == "failure":
        run_failure_workflow()
    if REQUESTED_STATE == "cancel":
        run_cancel_workflow()
    if REQUESTED_STATE == "recovery":
        run_recovery_workflow()
    if REQUESTED_STATE != "success":
        raise RuntimeError("Unsupported workflow state: " + REQUESTED_STATE)
    progress("activate-workbench:start")
    Gui.activateWorkbench("PartDesignWorkbench")
    flush_gui()
    progress("activate-workbench:done")
    document = App.newDocument("PadGuiSuccess")
    document.UndoMode = 1
    body = document.addObject("PartDesign::Body", "Body")
    sketch = body.newObject("Sketcher::SketchObject", "Sketch")
    points = [
        App.Vector(0, 0, 0),
        App.Vector(4, 0, 0),
        App.Vector(4, 3, 0),
        App.Vector(0, 3, 0),
    ]
    for index in range(4):
        sketch.addGeometry(Part.LineSegment(points[index], points[(index + 1) % 4]), False)
    document.recompute()
    Gui.activeView().setActiveObject("pdbody", body)
    Gui.Selection.clearSelection()
    Gui.Selection.addSelection(document.Name, sketch.Name)
    flush_gui()
    progress("fixture:done")

    command = Gui.Command.get("PartDesign_Pad")
    actions = list(command.getAction()) if command else []
    before = {
        "activeWorkbench": str(Gui.activeWorkbench().name()),
        "commandRegistered": command is not None,
        "commandActive": bool(command.isActive()) if command else False,
        "actionCount": len(actions),
        "actionEnabled": bool(actions[0].isEnabled()) if actions else None,
        "selection": selection_names(),
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "undoNames": available_transactions(document, "getAvailableUndoNames"),
    }

    progress("run-command:start")
    Gui.runCommand("PartDesign_Pad", 0)
    flush_gui()
    progress("run-command:done")
    pad = document.getObject("Pad")
    opened = task_panel_state("opened")
    if pad is None:
        raise RuntimeError("PartDesign_Pad did not create a Pad preview object")
    pad.Length = 10.0
    document.recompute()
    flush_gui()
    progress("preview:done")
    preview = {
        "name": str(pad.Name),
        "typeId": str(pad.TypeId),
        "length": round(float(pad.Length.Value), 9),
        "shapeValid": bool(pad.Shape.isValid()),
        "solidCount": len(pad.Shape.Solids),
        "volume": round(float(pad.Shape.Volume), 9),
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "profile": profile_name(pad),
    }
    progress("accept:start")
    acceptClicked = click_accept()
    progress("accept:done")
    document.recompute()
    flush_gui()
    closed = task_panel_state("closed")
    pad = document.getObject("Pad")
    after = {
        "acceptClicked": acceptClicked,
        "selection": selection_names(),
        "objectNames": [str(obj.Name) for obj in document.Objects],
        "bodyGroup": [str(obj.Name) for obj in body.Group],
        "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
        "pad": {
            "name": str(pad.Name),
            "typeId": str(pad.TypeId),
            "length": round(float(pad.Length.Value), 9),
            "shapeValid": bool(pad.Shape.isValid()),
            "solidCount": len(pad.Shape.Solids),
            "faceCount": len(pad.Shape.Faces),
            "edgeCount": len(pad.Shape.Edges),
            "vertexCount": len(pad.Shape.Vertexes),
            "volume": round(float(pad.Shape.Volume), 9),
            "profile": profile_name(pad),
            "state": [str(value) for value in pad.State],
        },
        "undoCount": counter(document, "UndoCount"),
        "redoCount": counter(document, "RedoCount"),
        "undoNames": available_transactions(document, "getAvailableUndoNames"),
        "redoNames": available_transactions(document, "getAvailableRedoNames"),
    }
    version = App.Version()
    result = {
        "schemaVersion": 1,
        "baselineId": "freecad-1.1.1",
        "freecadVersion": ".".join(str(value) for value in version[:3]),
        "gitCommit": str(version[7]) if len(version) > 7 else "",
        "workflowId": "partdesign-pad-task",
        "commandId": "PartDesign_Pad",
        "state": REQUESTED_STATE,
        "before": before,
        "taskPanel": {
            "opened": opened,
            "preview": preview,
            "closed": closed,
        },
        "after": after,
        "success": bool(
            opened["activeDialog"]
            and opened["inEdit"] == "Pad"
            and acceptClicked
            and not closed["activeDialog"]
            and not closed["inEdit"]
            and after["bodyTip"] == "Pad"
            and after["pad"]["shapeValid"]
            and after["pad"]["solidCount"] == 1
            and abs(after["pad"]["volume"] - 120.0) < 1e-7
        ),
    }
    progress("result:done")
    App.closeDocument(document.Name)
    emit(result, 0 if result["success"] else 2)
except Exception as error:
    progress("error:" + type(error).__name__)
    emit({
        "schemaVersion": 1,
        "baselineId": "freecad-1.1.1",
        "workflowId": "partdesign-pad-task",
        "commandId": "PartDesign_Pad",
        "state": REQUESTED_STATE,
        "success": False,
        "error": {"type": type(error).__name__, "message": str(error)},
    }, 2)
