#!/usr/bin/env python3
# subsystem-info v1.6 — Compact summary of 1C subsystem structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills

import argparse
import os
import re
import sys
from collections import OrderedDict
from lxml import etree

sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")

# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
    """parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
    argv = list(sys.argv[1:] if argv is None else argv)
    names = {s.lower(): s for a in parser._actions for s in a.option_strings}
    for i, tok in enumerate(argv):
        if tok.startswith('-') and tok.lower() in names:
            argv[i] = names[tok.lower()]
    # choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
    choice_map = {}
    for a in parser._actions:
        if a.choices:
            for s in a.option_strings:
                choice_map[s] = {str(c).lower(): c for c in a.choices}
    for i in range(len(argv) - 1):
        m = choice_map.get(argv[i])
        if m and argv[i + 1].lower() in m:
            argv[i + 1] = m[argv[i + 1].lower()]
    return parser.parse_args(argv)


# --- Argument parsing ---
parser = argparse.ArgumentParser(description="Analyze 1C subsystem structure", allow_abbrev=False)
parser.add_argument("-SubsystemPath", "-Path", required=True, help="Path to subsystem XML or Subsystems/ directory")
parser.add_argument("-Mode", choices=["overview", "content", "ci", "tree", "full"], default="overview", help="Output mode")
parser.add_argument("-Name", default="", help="Filter by name/type")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
args = ci_parse_args(parser)

# --- Output helper ---
lines_buf = []

def out(text=""):
    lines_buf.append(text)

# --- Resolve path ---
subsystem_path = args.SubsystemPath
if not os.path.isabs(subsystem_path):
    subsystem_path = os.path.join(os.getcwd(), subsystem_path)

NS = {
    "md": "http://v8.1c.ru/8.3/MDClasses",
    "v8": "http://v8.1c.ru/8.1/data/core",
    "xr": "http://v8.1c.ru/8.3/xcf/readable",
    "xsi": "http://www.w3.org/2001/XMLSchema-instance",
}

CI_NS = {
    "ci": "http://v8.1c.ru/8.3/xcf/extrnprops",
    "xr": "http://v8.1c.ru/8.3/xcf/readable",
}

# --- Helper: get LocalString text ---
def get_ml_text(node):
    if node is None:
        return ""
    # Look for v8:item children
    for item in node:
        if not isinstance(item.tag, str):
            continue
        lang = ""
        content = ""
        for c in item:
            if not isinstance(c.tag, str):
                continue
            local = etree.QName(c.tag).localname
            if local == "lang":
                lang = c.text or ""
            if local == "content":
                content = c.text or ""
        if lang == "ru" and content:
            return content
    # fallback: first item
    for item in node:
        if not isinstance(item.tag, str):
            continue
        for c in item:
            if not isinstance(c.tag, str):
                continue
            local = etree.QName(c.tag).localname
            if local == "content" and c.text:
                return c.text
    return ""

# --- Helper: load subsystem XML ---
def load_subsystem_xml(xml_path):
    tree = etree.parse(xml_path, etree.XMLParser(remove_blank_text=False))
    doc_root = tree.getroot()
    sub = doc_root.find("md:Subsystem", NS)
    if sub is None:
        print(f"[ERROR] Not a valid subsystem XML: {xml_path}")
        sys.exit(1)
    return {"Doc": doc_root, "Sub": sub}

# --- Helper: get content items ---
def get_content_items(props):
    items = []
    content_node = props.find("md:Content", NS)
    if content_node is None:
        return items
    for item in content_node.findall("xr:Item", NS):
        if item.text:
            items.append(item.text)
    return items

# --- Helper: get child subsystem names ---
def get_child_names(sub):
    names = []
    co = sub.find("md:ChildObjects", NS)
    if co is None:
        return names
    for child in co:
        if not isinstance(child.tag, str):
            continue
        if etree.QName(child.tag).localname == "Subsystem":
            names.append(child.text or "")
    return names

# --- Helper: group content by type ---
def group_content_by_type(items):
    groups = OrderedDict()
    for item in items:
        m = re.match(r'^([^.]+)\.(.+)$', item)
        if m:
            type_name = m.group(1)
            name = m.group(2)
        elif re.match(r'^[0-9a-fA-F]{8}-', item):
            type_name = "[UUID]"
            name = item
        else:
            type_name = "[Other]"
            name = item
        if type_name not in groups:
            groups[type_name] = []
        groups[type_name].append(name)
    return groups

# --- Helper: find subsystem dir from XML path ---
def get_subsystem_dir(xml_path):
    dir_name = os.path.dirname(xml_path)
    base_name = os.path.splitext(os.path.basename(xml_path))[0]
    return os.path.join(dir_name, base_name)

# --- Show functions ---
def get_support_status_for_path(target_path):
    try:
        def root_uuid(xml_path):
            if not os.path.isfile(xml_path):
                return None
            try:
                mx = etree.parse(xml_path).getroot()
                for child in mx:
                    if isinstance(child.tag, str) and child.get("uuid"):
                        return child.get("uuid")
            except Exception:
                pass
            return None
        def _sg_is_external_root(xml_path):
            if not os.path.isfile(xml_path):
                return False
            try:
                mx = etree.parse(xml_path).getroot()
                for child in mx:
                    if isinstance(child.tag, str):
                        return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
            except Exception:
                return False
            return False
        rp = os.path.abspath(target_path)
        # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
        elem_uuid = root_uuid(rp)
        if _sg_is_external_root(rp):
            return None
        bin_path = None
        d = os.path.dirname(rp)
        for _ in range(12):
            if not d:
                break
            if _sg_is_external_root(d + ".xml"):
                return None
            if not elem_uuid:
                elem_uuid = root_uuid(d + ".xml")
            if not bin_path:
                cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
                if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
                    bin_path = cand
            if elem_uuid and bin_path:
                break
            parent = os.path.dirname(d)
            if parent == d:
                break
            d = parent
        if not bin_path or not os.path.exists(bin_path):
            return "не на поддержке"
        data = open(bin_path, "rb").read()
        if len(data) <= 32:
            return "снято с поддержки (правки свободны)"
        if data[:3] == b"\xef\xbb\xbf":
            data = data[3:]
        text = data.decode("utf-8", "replace")
        h = re.match(r"\{6,(\d+),(\d+),", text)
        if not h:
            return "не на поддержке"
        g = int(h.group(1))
        k = int(h.group(2))
        if k == 0:
            return "снято с поддержки (правки свободны)"
        if g == 1:
            return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
        if not elem_uuid:
            return "не на поддержке"
        best = None
        for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
            f1 = int(m.group(1))
            if best is None or f1 < best:
                best = f1
        if best is None:
            return "не на поддержке"
        return {
            0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
            1: "редактируется с сохранением поддержки",
            2: "снято с поддержки (правки свободны)",
        }.get(best, "не на поддержке")
    except Exception:
        return "не на поддержке"


def show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
                  explanation, pic_text, content_items, groups, child_names, has_ci):
    out(f"Подсистема: {sub_name}")
    _support = get_support_status_for_path(subsystem_path)
    if _support is not None:
        out(f"Поддержка: {_support}")
    if synonym and synonym != sub_name:
        out(f"Синоним: {synonym}")
    if comment_text:
        out(f"Комментарий: {comment_text}")
    out(f"ВключатьВКомандныйИнтерфейс: {incl_ci}")
    out(f"ИспользоватьОднуКоманду: {use_one_cmd}")
    if explanation:
        out(f"Пояснение: {explanation}")
    if pic_text:
        out(f"Картинка: {pic_text}")
    if len(content_items) > 0:
        parts = []
        for type_name in groups:
            parts.append(f"{type_name}: {len(groups[type_name])}")
        out(f"Состав: {len(content_items)} объектов ({', '.join(parts)})")
    else:
        out("Состав: пусто")
    if len(child_names) > 0:
        out(f"Дочерние подсистемы ({len(child_names)}): {', '.join(child_names)}")
    if has_ci:
        out("Командный интерфейс: есть")


def show_content(sub_name, content_items, groups, name_filter):
    out(f"Состав подсистемы {sub_name} ({len(content_items)} объектов):")
    out()
    if name_filter:
        if name_filter in groups:
            filtered = groups[name_filter]
            out(f"{name_filter} ({len(filtered)}):")
            for n in filtered:
                out(f"  {n}")
        else:
            out(f"[INFO] Тип '{name_filter}' не найден в составе.")
            out(f"Доступные типы: {', '.join(groups.keys())}")
    else:
        for type_name in groups:
            out(f"{type_name} ({len(groups[type_name])}):")
            for n in groups[type_name]:
                out(f"  {n}")
            out()


def show_ci(sub_name, subsystem_path_local):
    local_sub_dir = get_subsystem_dir(subsystem_path_local)
    local_ci_path = os.path.join(local_sub_dir, "Ext", "CommandInterface.xml")

    if not os.path.isfile(local_ci_path):
        out(f"Командный интерфейс: {sub_name}")
        out()
        out("Файл CommandInterface.xml не найден.")
        out(f"Путь: {local_ci_path}")
    else:
        ci_tree = etree.parse(local_ci_path, etree.XMLParser(remove_blank_text=False))
        ci_root = ci_tree.getroot()

        out(f"Командный интерфейс: {sub_name}")
        out()

        # --- CommandsVisibility ---
        vis_section = ci_root.find("ci:CommandsVisibility", CI_NS)
        if vis_section is not None:
            hidden = []
            shown = []
            for cmd in vis_section.findall("ci:Command", CI_NS):
                cmd_name = cmd.get("name", "")
                vis = cmd.find("ci:Visibility/xr:Common", CI_NS)
                if vis is not None and vis.text == "false":
                    hidden.append(cmd_name)
                else:
                    shown.append(cmd_name)
            total = len(hidden) + len(shown)
            if not args.Name or args.Name == "visibility":
                out(f"Видимость ({total}):")
                if hidden:
                    out(f"  СКРЫТО ({len(hidden)}):")
                    for h in hidden:
                        out(f"    {h}")
                if shown:
                    out(f"  ПОКАЗАНО ({len(shown)}):")
                    for s in shown:
                        out(f"    {s}")
                out()

        # --- CommandsPlacement ---
        place_section = ci_root.find("ci:CommandsPlacement", CI_NS)
        if place_section is not None:
            placements = []
            for cmd in place_section.findall("ci:Command", CI_NS):
                cmd_name = cmd.get("name", "")
                grp = cmd.find("ci:CommandGroup", CI_NS)
                pl = cmd.find("ci:Placement", CI_NS)
                grp_text = grp.text if grp is not None and grp.text else "?"
                pl_text = pl.text if pl is not None and pl.text else "?"
                placements.append({"Name": cmd_name, "Group": grp_text, "Placement": pl_text})
            if (not args.Name or args.Name == "placement") and placements:
                arrow = "\u2192"
                out(f"Размещение ({len(placements)}):")
                for p in placements:
                    out(f"  {p['Name']} {arrow} {p['Group']} ({p['Placement']})")
                out()

        # --- CommandsOrder ---
        order_section = ci_root.find("ci:CommandsOrder", CI_NS)
        if order_section is not None:
            order_groups = OrderedDict()
            for cmd in order_section.findall("ci:Command", CI_NS):
                cmd_name = cmd.get("name", "")
                grp = cmd.find("ci:CommandGroup", CI_NS)
                grp_text = grp.text if grp is not None and grp.text else "?"
                if grp_text not in order_groups:
                    order_groups[grp_text] = []
                order_groups[grp_text].append(cmd_name)
            total_order = sum(len(v) for v in order_groups.values())
            if (not args.Name or args.Name == "order") and total_order > 0:
                out(f"Порядок команд ({total_order}):")
                for grp_name, cmds in order_groups.items():
                    out(f"  [{grp_name}]:")
                    for c in cmds:
                        out(f"    {c}")
                out()

        # --- SubsystemsOrder ---
        sub_order_section = ci_root.find("ci:SubsystemsOrder", CI_NS)
        if sub_order_section is not None:
            sub_order = []
            for s in sub_order_section.findall("ci:Subsystem", CI_NS):
                if s.text:
                    sub_order.append(s.text)
            if (not args.Name or args.Name == "subsystems") and sub_order:
                out(f"Порядок подсистем ({len(sub_order)}):")
                for i, s in enumerate(sub_order):
                    out(f"  {i + 1}. {s}")
                out()

        # --- GroupsOrder ---
        grp_order_section = ci_root.find("ci:GroupsOrder", CI_NS)
        if grp_order_section is not None:
            grp_order = []
            for g in grp_order_section.findall("ci:Group", CI_NS):
                if g.text:
                    grp_order.append(g.text)
            if (not args.Name or args.Name == "groups") and grp_order:
                out(f"Порядок групп ({len(grp_order)}):")
                for g in grp_order:
                    out(f"  {g}")


# ============================================================
# Mode: tree
# ============================================================
if args.Mode == "tree":
    is_dir = os.path.isdir(subsystem_path)
    root_dir = None
    root_xml = None

    if is_dir:
        root_dir = subsystem_path
    else:
        if not os.path.isfile(subsystem_path):
            print(f"[ERROR] File not found: {subsystem_path}")
            sys.exit(1)
        root_xml = subsystem_path

    # Box-drawing chars
    T_BRANCH = "\u251C\u2500\u2500 "   # ├──
    T_LAST   = "\u2514\u2500\u2500 "   # └──
    T_PIPE   = "\u2502   "             # │
    T_ARROW  = "\u2192"                # →

    def get_tree_line(xml_path):
        parsed = load_subsystem_xml(xml_path)
        sub = parsed["Sub"]
        props = sub.find("md:Properties", NS)
        name_node = props.find("md:Name", NS)
        name = name_node.text if name_node is not None else ""

        markers = []
        sub_dir = get_subsystem_dir(xml_path)
        ci_path = os.path.join(sub_dir, "Ext", "CommandInterface.xml")
        if os.path.isfile(ci_path):
            markers.append("CI")
        use_one = props.find("md:UseOneCommand", NS)
        if use_one is not None and use_one.text == "true":
            markers.append("OneCmd")
        incl_ci_node = props.find("md:IncludeInCommandInterface", NS)
        if incl_ci_node is not None and incl_ci_node.text == "false":
            markers.append("Скрыт")
        marker_str = f" [{', '.join(markers)}]" if markers else ""

        content_items = get_content_items(props)
        child_names = get_child_names(sub)
        child_str = f", {len(child_names)} дочерних" if child_names else ""

        return {
            "Label": f"{name}{marker_str} ({len(content_items)} объектов{child_str})",
            "SubDir": sub_dir,
            "ChildNames": child_names,
        }

    def build_tree_entry(xml_path, prefix, is_last, is_root):
        info = get_tree_line(xml_path)

        if is_root:
            connector = ""
        elif is_last:
            connector = T_LAST
        else:
            connector = T_BRANCH
        out(f"{prefix}{connector}{info['Label']}")

        if info["ChildNames"]:
            if is_root:
                child_prefix = ""
            elif is_last:
                child_prefix = prefix + "    "
            else:
                child_prefix = prefix + T_PIPE

            subs_dir = os.path.join(info["SubDir"], "Subsystems")
            for i, child_name in enumerate(info["ChildNames"]):
                child_xml = os.path.join(subs_dir, f"{child_name}.xml")
                child_is_last = (i == len(info["ChildNames"]) - 1)
                if os.path.isfile(child_xml):
                    build_tree_entry(child_xml, child_prefix, child_is_last, False)
                else:
                    conn2 = T_LAST if child_is_last else T_BRANCH
                    out(f"{child_prefix}{conn2}{child_name} [NOT FOUND]")

    if root_dir:
        label = os.path.basename(root_dir)
        out(f"Дерево подсистем от: {label}/")
        out()
        xml_files = sorted(
            [f for f in os.listdir(root_dir) if f.lower().endswith(".xml") and os.path.isfile(os.path.join(root_dir, f))],
            key=lambda x: x.lower()
        )
        if args.Name:
            xml_files = [f for f in xml_files if os.path.splitext(f)[0] == args.Name]
            if not xml_files:
                print(f"[ERROR] Subsystem '{args.Name}' not found in {root_dir}")
                sys.exit(1)
        for i, fname in enumerate(xml_files):
            build_tree_entry(os.path.join(root_dir, fname), "", i == len(xml_files) - 1, True)
    else:
        build_tree_entry(root_xml, "", True, True)

elif args.Mode == "ci":
    # ============================================================
    # Mode: ci -- CommandInterface.xml
    # ============================================================
    if os.path.isdir(subsystem_path):
        print("[ERROR] ci mode requires a subsystem .xml file, not a directory")
        sys.exit(1)
    # File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
    if not os.path.isfile(subsystem_path):
        fn = os.path.splitext(os.path.basename(subsystem_path))[0]
        pd = os.path.dirname(subsystem_path)
        if fn == os.path.basename(pd):
            c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
            if os.path.isfile(c):
                subsystem_path = c
    if not os.path.isfile(subsystem_path):
        print(f"[ERROR] File not found: {subsystem_path}")
        sys.exit(1)

    parsed = load_subsystem_xml(subsystem_path)
    sub = parsed["Sub"]
    props = sub.find("md:Properties", NS)
    name_node = props.find("md:Name", NS)
    sub_name = name_node.text if name_node is not None else ""

    show_ci(sub_name, subsystem_path)

else:
    # ============================================================
    # Mode: overview / content / full -- requires a subsystem XML file
    # ============================================================
    if os.path.isdir(subsystem_path):
        dir_name = os.path.basename(subsystem_path)
        candidate = os.path.join(subsystem_path, f"{dir_name}.xml")
        sibling = os.path.join(os.path.dirname(subsystem_path), f"{dir_name}.xml")
        if os.path.isfile(candidate):
            subsystem_path = candidate
        elif os.path.isfile(sibling):
            subsystem_path = sibling
        else:
            print(f"[ERROR] No {dir_name}.xml found in directory. Use -Mode tree for directory listing.")
            sys.exit(1)

    # File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
    if not os.path.isfile(subsystem_path):
        fn = os.path.splitext(os.path.basename(subsystem_path))[0]
        pd = os.path.dirname(subsystem_path)
        if fn == os.path.basename(pd):
            c = os.path.join(os.path.dirname(pd), f"{fn}.xml")
            if os.path.isfile(c):
                subsystem_path = c
    if not os.path.isfile(subsystem_path):
        print(f"[ERROR] File not found: {subsystem_path}")
        sys.exit(1)

    parsed = load_subsystem_xml(subsystem_path)
    sub = parsed["Sub"]
    props = sub.find("md:Properties", NS)

    name_node = props.find("md:Name", NS)
    sub_name = name_node.text if name_node is not None else ""
    synonym = get_ml_text(props.find("md:Synonym", NS))
    comment_node = props.find("md:Comment", NS)
    comment_text = comment_node.text if comment_node is not None and comment_node.text else ""
    incl_help_node = props.find("md:IncludeHelpInContents", NS)
    incl_help = incl_help_node.text if incl_help_node is not None else ""
    incl_ci_node = props.find("md:IncludeInCommandInterface", NS)
    incl_ci = incl_ci_node.text if incl_ci_node is not None else ""
    use_one_cmd_node = props.find("md:UseOneCommand", NS)
    use_one_cmd = use_one_cmd_node.text if use_one_cmd_node is not None else ""
    explanation = get_ml_text(props.find("md:Explanation", NS))

    # Picture
    pic_node = props.find("md:Picture", NS)
    pic_text = ""
    if pic_node is not None and len(pic_node) > 0:
        pic_ref = pic_node.find("xr:Ref", NS)
        if pic_ref is not None and pic_ref.text:
            pic_text = pic_ref.text

    # Content
    content_items = get_content_items(props)
    groups = group_content_by_type(content_items)

    # Children
    child_names = get_child_names(sub)

    # CI presence
    sub_dir = get_subsystem_dir(subsystem_path)
    ci_path = os.path.join(sub_dir, "Ext", "CommandInterface.xml")
    has_ci = os.path.isfile(ci_path)

    if args.Mode == "overview":
        show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
                      explanation, pic_text, content_items, groups, child_names, has_ci)
    elif args.Mode == "content":
        show_content(sub_name, content_items, groups, args.Name)
    elif args.Mode == "full":
        show_overview(sub_name, synonym, comment_text, incl_ci, use_one_cmd,
                      explanation, pic_text, content_items, groups, child_names, has_ci)
        out()
        out("--- content ---")
        out()
        show_content(sub_name, content_items, groups, args.Name)
        out()
        out("--- ci ---")
        out()
        show_ci(sub_name, subsystem_path)

# --- Pagination and output ---
total_lines = len(lines_buf)
out_lines = lines_buf[:]

if args.Offset > 0:
    if args.Offset >= total_lines:
        print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.")
        sys.exit(0)
    out_lines = out_lines[args.Offset:]

if args.Limit > 0 and len(out_lines) > args.Limit:
    shown = out_lines[:args.Limit]
    remaining = total_lines - args.Offset - args.Limit
    shown.append("")
    shown.append(f"[ОБРЕЗАНО] Показано {args.Limit} из {total_lines} строк. Используйте -Offset {args.Offset + args.Limit} для продолжения.")
    out_lines = shown

if args.OutFile:
    out_file = args.OutFile
    if not os.path.isabs(out_file):
        out_file = os.path.join(os.getcwd(), out_file)
    with open(out_file, "w", encoding="utf-8-sig") as f:
        f.write("\n".join(out_lines))
    print(f"Output written to {out_file}")
else:
    for line in out_lines:
        print(line)
