#!/usr/bin/env python3
"""Safely change CommandName for an existing 1C managed form button."""

from __future__ import annotations

import argparse
import html
import json
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any

from edit_1c_form_button import (
    attr_value,
    ensure_inside_working,
    is_void_or_self,
    load_json,
    local_name,
    manifest_record,
    normalize_command_name,
    read_text,
    render_reindented,
    safe_relative,
    tag_local,
    write_json,
    xml_text,
    TAG_RE,
)
from validate_1c_patch_workspace_semantics import child_text, validate_workspace


def form_command_exists(path: Path, command_name: str) -> bool:
    normalized = normalize_command_name(command_name)
    if normalized.startswith("Form.StandardCommand."):
        return True
    local_command_name = normalized.removeprefix("Form.Command.")
    root = ET.parse(path).getroot()
    return any(local_name(element.tag) == "Command" and element.attrib.get("name") == local_command_name for element in root.iter())


def button_state(path: Path, button_name: str) -> dict[str, Any] | None:
    root = ET.parse(path).getroot()
    for element in root.iter():
        if local_name(element.tag) == "Button" and element.attrib.get("name") == button_name:
            return {"name": button_name, "id": element.attrib.get("id"), "command_name": child_text(element, "CommandName")}
    return None


def find_button_span(text: str, button_name: str) -> tuple[int, int, int, int, str]:
    for match in TAG_RE.finditer(text):
        if match.group("close") or is_void_or_self(match):
            continue
        if tag_local(match.group("tag")) != "Button":
            continue
        if attr_value(match.group("attrs") or "", "name") != button_name:
            continue
        stack = ["Button"]
        for next_match in TAG_RE.finditer(text, match.end()):
            next_tag = tag_local(next_match.group("tag"))
            if next_match.group("close"):
                if stack and stack[-1] == next_tag:
                    stack.pop()
                    if not stack:
                        line_start = text.rfind("\n", 0, match.start()) + 1
                        indent = re.match(r"^[ \t]*", text[line_start : match.start()]).group(0)
                        return match.start(), next_match.end(), match.end(), next_match.start(), indent
                continue
            if not is_void_or_self(next_match):
                stack.append(next_tag)
        raise SystemExit(f"Could not find closing </Button> for: {button_name}")
    raise SystemExit(f"Form button not found by name: {button_name}")


def replace_or_insert_command_name(text: str, *, button_name: str, command_name: str) -> tuple[str, str, str | None]:
    _button_start, _button_end, start_tag_end, end_tag_start, button_indent = find_button_span(text, button_name)
    body = text[start_tag_end:end_tag_start]
    normalized = normalize_command_name(command_name)
    command_re = re.compile(r"(?P<indent>^[ \t]*)<CommandName\b[^>]*>(?P<value>[\s\S]*?)</CommandName>[ \t]*(?:\r?\n)?", re.MULTILINE)
    match = command_re.search(body)
    if match:
        old = html.unescape(match.group("value").strip())
        replacement = f"{match.group('indent')}<CommandName>{xml_text(normalized)}</CommandName>\n"
        updated_body = body[: match.start()] + replacement + body[match.end() :]
        return text[:start_tag_end] + updated_body + text[end_tag_start:], "replaced", old
    insert = render_reindented(f"<CommandName>{xml_text(normalized)}</CommandName>", button_indent + "\t") + "\n"
    return text[:start_tag_end] + "\n" + insert + text[start_tag_end:], "inserted", None


def validate_form_xml(path: Path, before: str, *, keep_on_failure: bool) -> tuple[bool, bool, list[dict[str, Any]]]:
    findings: list[dict[str, Any]] = []
    rolled_back = False
    try:
        ET.parse(path)
        return True, False, findings
    except ET.ParseError as exc:
        findings.append({"severity": "error", "message": str(exc), "path": str(path)})
        if not keep_on_failure:
            path.write_text(before, encoding="utf-8")
            rolled_back = True
        return False, rolled_back, findings


def edit_form_file(
    path: Path,
    *,
    button_name: str,
    command_name: str,
    keep_on_failure: bool,
) -> dict[str, Any]:
    form_path = path.resolve()
    if not form_path.exists():
        raise SystemExit(f"Form.xml does not exist: {form_path}")
    if not form_command_exists(form_path, command_name):
        raise SystemExit(f"Form command does not exist: {command_name}")
    state = button_state(form_path, button_name)
    if not state:
        raise SystemExit(f"Form button does not exist: {button_name}")
    before = read_text(form_path)
    updated, status, old = replace_or_insert_command_name(before, button_name=button_name, command_name=command_name)
    form_path.write_text(updated, encoding="utf-8")
    passed, rolled_back, findings = validate_form_xml(form_path, before, keep_on_failure=keep_on_failure)
    return {
        "schema": "onec_form_button_command_edit.v1",
        "path": str(form_path),
        "edit": {
            "status": status,
            "button": {
                "name": button_name,
                "id": state.get("id"),
                "old_command_name": old if old is not None else state.get("command_name"),
                "command_name": normalize_command_name(command_name),
            },
        },
        "rolled_back": rolled_back,
        "semantic_validation": {
            "schema": "onec_form_xml_direct_validation.v1",
            "passed": passed,
            "counts": {"findings": len(findings)},
            "findings": findings,
        },
        "passed": passed and not rolled_back,
    }


def edit_workspace(
    workspace: Path,
    relative_path: str,
    *,
    button_name: str,
    command_name: str,
    keep_on_failure: bool,
) -> dict[str, Any]:
    record = manifest_record(workspace, relative_path)
    path = ensure_inside_working(workspace, str(record.get("relative_path") or relative_path))
    before = read_text(path)
    result = edit_form_file(path, button_name=button_name, command_name=command_name, keep_on_failure=True)
    semantic = validate_workspace(workspace)
    rolled_back = False
    if not semantic.get("passed") and not keep_on_failure:
        path.write_text(before, encoding="utf-8")
        rolled_back = True
    result.update(
        {
            "workspace": str(workspace),
            "relative_path": str(record.get("relative_path")),
            "rolled_back": rolled_back,
            "semantic_validation": {
                "schema": semantic.get("schema"),
                "passed": semantic.get("passed"),
                "counts": semantic.get("counts"),
                "findings": semantic.get("findings"),
            },
            "passed": bool(semantic.get("passed")) and not rolled_back,
        }
    )
    return result


def main() -> int:
    parser = argparse.ArgumentParser(description="Change CommandName for an existing button in 1C managed Form.xml.")
    parser.add_argument("--workspace", type=Path)
    parser.add_argument("--relative-path")
    parser.add_argument("--form-path", type=Path, help="Direct Form.xml path for extension-export XML editing.")
    parser.add_argument("--button-name", "--name", dest="button_name", required=True)
    parser.add_argument("--command-name", required=True)
    parser.add_argument("--keep-on-failure", action="store_true")
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    if args.form_path:
        result = edit_form_file(
            args.form_path,
            button_name=args.button_name,
            command_name=args.command_name,
            keep_on_failure=args.keep_on_failure,
        )
    else:
        if not args.workspace or not args.relative_path:
            parser.error("either --form-path or both --workspace and --relative-path are required")
        result = edit_workspace(
            args.workspace,
            safe_relative(args.relative_path),
            button_name=args.button_name,
            command_name=args.command_name,
            keep_on_failure=args.keep_on_failure,
        )
    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"], "edit": result["edit"]}, ensure_ascii=False))
    return 0 if result["passed"] else 2


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