#!/usr/bin/env python3
"""Atomically add a BSL handler, form command, and visible form button."""

from __future__ import annotations

import argparse
import base64
import json
from pathlib import Path
from typing import Any

from diff_1c_patch_workspace import build_diff
from edit_1c_bsl_routine import edit_workspace as edit_bsl_routine
from edit_1c_form_button import edit_workspace as edit_form_button
from edit_1c_form_command import edit_workspace as edit_form_command
from validate_1c_patch_workspace_semantics import validate_workspace


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8-sig"))


def write_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def read_bytes(path: Path) -> bytes:
    return path.read_bytes()


def workspace_working_files(workspace: Path) -> list[Path]:
    manifest = load_json(workspace / "manifest.json")
    paths = []
    for record in manifest.get("files") or []:
        relative = Path(str(record.get("relative_path") or ""))
        if relative.is_absolute() or ".." in relative.parts:
            raise SystemExit(f"Unsafe manifest relative path: {relative}")
        path = workspace / "working" / relative
        if path.exists():
            paths.append(path)
    return paths


def snapshot_working_files(workspace: Path) -> dict[Path, bytes]:
    return {path: read_bytes(path) for path in workspace_working_files(workspace)}


def restore_snapshot(snapshot: dict[Path, bytes]) -> None:
    for path, content in snapshot.items():
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_bytes(content)


def decode_routine_text(args: argparse.Namespace) -> str:
    sources = [bool(args.routine_text), bool(args.routine_text_b64), bool(args.routine_file)]
    if sum(sources) != 1:
        raise SystemExit("Use exactly one of --routine-text, --routine-text-b64, or --routine-file.")
    if args.routine_text is not None:
        return args.routine_text
    if args.routine_text_b64:
        return base64.b64decode(args.routine_text_b64).decode("utf-8")
    return Path(args.routine_file).read_text(encoding="utf-8-sig")


def build_result(
    *,
    workspace: Path,
    form_relative_path: str,
    bsl_relative_path: str,
    operation: str,
    routine_text: str,
    command_name: str,
    command_title: str,
    command_action: str,
    button_parent_name: str,
    button_name: str,
    button_title: str,
    keep_on_failure: bool,
    max_patch_chars: int,
) -> dict[str, Any]:
    snapshot = snapshot_working_files(workspace)
    steps: list[dict[str, Any]] = []
    rolled_back = False
    error: dict[str, Any] | None = None

    try:
        bsl = edit_bsl_routine(
            workspace,
            bsl_relative_path,
            routine_text,
            operation=operation,
            keep_on_failure=True,
        )
        steps.append({"name": "bsl_routine", "result": bsl})
        if not bsl.get("semantic_validation", {}).get("passed"):
            raise RuntimeError("BSL routine edit failed semantic validation.")

        command = edit_form_command(
            workspace,
            form_relative_path,
            name=command_name,
            title=command_title,
            action=command_action,
            tooltip=None,
            command_id=None,
            call_type="Override",
            operation=operation,
            keep_on_failure=True,
        )
        steps.append({"name": "form_command", "result": command})
        if not command.get("semantic_validation", {}).get("passed"):
            raise RuntimeError("Form command edit failed semantic validation.")

        button = edit_form_button(
            workspace,
            form_relative_path,
            parent_name=button_parent_name,
            name=button_name,
            title=button_title,
            command_name=command_name,
            button_id=None,
            button_type="CommandBarButton",
            operation=operation,
            keep_on_failure=True,
        )
        steps.append({"name": "form_button", "result": button})
        if not button.get("semantic_validation", {}).get("passed"):
            raise RuntimeError("Form button edit failed semantic validation.")

        semantic = validate_workspace(workspace)
        if not semantic.get("passed"):
            raise RuntimeError("Final semantic validation failed.")
        diff = build_diff(workspace, max_patch_chars=max_patch_chars)
    except (Exception, SystemExit) as exc:
        error = {"type": type(exc).__name__, "message": str(exc)}
        semantic = validate_workspace(workspace)
        diff = build_diff(workspace, max_patch_chars=max_patch_chars)
        if not keep_on_failure:
            restore_snapshot(snapshot)
            rolled_back = True
            semantic = validate_workspace(workspace)
            diff = build_diff(workspace, max_patch_chars=max_patch_chars)

    passed = error is None and bool(semantic.get("passed")) and bool(diff.get("passed"))
    return {
        "schema": "onec_form_button_workflow.v1",
        "workspace": str(workspace),
        "operation": operation,
        "inputs": {
            "form_relative_path": form_relative_path,
            "bsl_relative_path": bsl_relative_path,
            "command_name": command_name,
            "command_title": command_title,
            "command_action": command_action,
            "button_parent_name": button_parent_name,
            "button_name": button_name,
            "button_title": button_title,
        },
        "passed": passed,
        "rolled_back": rolled_back,
        "error": error,
        "steps": steps,
        "semantic_validation": {
            "schema": semantic.get("schema"),
            "passed": semantic.get("passed"),
            "counts": semantic.get("counts"),
            "findings": semantic.get("findings"),
        },
        "diff_summary": diff.get("counts"),
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Atomically add BSL handler, form command, and visible button in a 1C patch workspace.")
    parser.add_argument("--workspace", type=Path, required=True)
    parser.add_argument("--form-relative-path", required=True)
    parser.add_argument("--bsl-relative-path", required=True)
    parser.add_argument("--operation", choices=["append", "replace", "upsert"], default="upsert")
    parser.add_argument("--routine-text")
    parser.add_argument("--routine-text-b64")
    parser.add_argument("--routine-file", type=Path)
    parser.add_argument("--command-name", required=True)
    parser.add_argument("--command-title", required=True)
    parser.add_argument("--command-action", required=True)
    parser.add_argument("--button-parent-name", required=True)
    parser.add_argument("--button-name", required=True)
    parser.add_argument("--button-title", required=True)
    parser.add_argument("--keep-on-failure", action="store_true", help="Keep partial edits when any step fails.")
    parser.add_argument("--max-patch-chars", type=int, default=200000)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    result = build_result(
        workspace=args.workspace,
        form_relative_path=args.form_relative_path,
        bsl_relative_path=args.bsl_relative_path,
        operation=args.operation,
        routine_text=decode_routine_text(args),
        command_name=args.command_name,
        command_title=args.command_title,
        command_action=args.command_action,
        button_parent_name=args.button_parent_name,
        button_name=args.button_name,
        button_title=args.button_title,
        keep_on_failure=args.keep_on_failure,
        max_patch_chars=args.max_patch_chars,
    )
    if args.output:
        write_json(args.output, result)
    print(
        json.dumps(
            {
                "output": str(args.output) if args.output else None,
                "passed": result["passed"],
                "rolled_back": result["rolled_back"],
                "error": result["error"],
                "semantic": result["semantic_validation"]["counts"],
                "diff": result["diff_summary"],
            },
            ensure_ascii=False,
        )
    )
    return 0 if result["passed"] else 2


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