#!/usr/bin/env python3
# mxl-info v1.5 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills

import argparse
import json
import os
import re
import sys
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 spreadsheet (MXL) structure", allow_abbrev=False)
parser.add_argument("-TemplatePath", "-Path", default="", help="Path to Template.xml")
parser.add_argument("-ProcessorName", default="", help="Processor name (used with -TemplateName)")
parser.add_argument("-TemplateName", default="", help="Template name (used with -ProcessorName)")
parser.add_argument("-SrcDir", default="src", help="Source directory (default: src)")
parser.add_argument("-Format", choices=["text", "json"], default="text", help="Output format")
parser.add_argument("-WithText", action="store_true", default=False, help="Include text content")
parser.add_argument("-MaxParams", type=int, default=10, help="Max parameters to show per area")
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")
args = ci_parse_args(parser)

# --- Resolve template path ---
template_path = args.TemplatePath

if not template_path:
    if not args.ProcessorName or not args.TemplateName:
        print("Specify -TemplatePath or both -ProcessorName and -TemplateName", file=sys.stderr)
        sys.exit(1)
    template_path = os.path.join(args.SrcDir, args.ProcessorName, "Templates", args.TemplateName, "Ext", "Template.xml")

if not os.path.isabs(template_path):
    template_path = os.path.join(os.getcwd(), template_path)

if not os.path.isfile(template_path):
    print(f"File not found: {template_path}", file=sys.stderr)
    sys.exit(1)

# --- Load XML ---
tree = etree.parse(template_path, etree.XMLParser(remove_blank_text=True))
root = tree.getroot()

NS = {
    "d": "http://v8.1c.ru/8.2/data/spreadsheet",
    "v8": "http://v8.1c.ru/8.1/data/core",
    "xsi": "http://www.w3.org/2001/XMLSchema-instance",
}

XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"

# --- Column sets ---
column_sets = []
default_col_count = 0

for cols in root.findall("d:columns", NS):
    size_node = cols.find("d:size", NS)
    id_node = cols.find("d:id", NS)
    size = int(size_node.text) if size_node is not None and size_node.text else 0

    if id_node is not None:
        column_sets.append({"Id": id_node.text or "", "Size": size})
    else:
        default_col_count = size

# --- Rows: collect row data ---
row_nodes = root.findall("d:rowsItem", NS)
total_rows = len(row_nodes)

height_node = root.find("d:height", NS)
doc_height = int(height_node.text) if height_node is not None and height_node.text else total_rows

# --- Named items ---
named_areas = []
named_drawings = []

for ni in root.findall("d:namedItem", NS):
    ni_type = ni.get(f"{{{XSI_NS}}}type", "")
    name_node = ni.find("d:name", NS)
    name = name_node.text if name_node is not None else ""

    if "NamedItemCells" in ni_type:
        area = ni.find("d:area", NS)
        area_type_node = area.find("d:type", NS)
        area_type = area_type_node.text if area_type_node is not None else ""
        begin_row = int(area.find("d:beginRow", NS).text)
        end_row = int(area.find("d:endRow", NS).text)
        begin_col = int(area.find("d:beginColumn", NS).text)
        end_col = int(area.find("d:endColumn", NS).text)
        cols_id = None
        cols_id_node = area.find("d:columnsID", NS)
        if cols_id_node is not None:
            cols_id = cols_id_node.text

        named_areas.append({
            "Name": name,
            "AreaType": area_type,
            "BeginRow": begin_row,
            "EndRow": end_row,
            "BeginCol": begin_col,
            "EndCol": end_col,
            "ColumnsID": cols_id,
        })
    elif "NamedItemDrawing" in ni_type:
        draw_id_node = ni.find("d:drawingID", NS)
        draw_id = draw_id_node.text if draw_id_node is not None else ""
        named_drawings.append({"Name": name, "DrawingID": draw_id})

# --- Scan rows for parameters and text ---

# Build row index map: rowIndex -> XmlNode
row_map = {}
for ri in row_nodes:
    idx_node = ri.find("d:index", NS)
    if idx_node is not None and idx_node.text:
        idx = int(idx_node.text)
        row_map[idx] = ri


def get_cell_data(row_node, include_text):
    row = row_node.find("d:row", NS)
    if row is None:
        return []

    results = []
    for c_group in row.findall("d:c", NS):
        cell = c_group.find("d:c", NS)
        if cell is None:
            continue

        param = cell.find("d:parameter", NS)
        detail = cell.find("d:detailParameter", NS)
        tl = cell.find("d:tl", NS)

        if param is not None:
            entry = {"Kind": "Parameter", "Value": param.text or ""}
            if detail is not None:
                entry["Detail"] = detail.text or ""
            results.append(entry)

        if tl is not None:
            content = tl.find("v8:item/v8:content", NS)
            if content is not None and content.text:
                text = content.text
                is_template = bool(re.search(r'\[.+\]', text))

                if is_template:
                    # Extract parameter names from [Param] placeholders
                    # Skip numeric-only like [5]
                    for m in re.finditer(r'\[([^\]]+)\]', text):
                        val = m.group(1)
                        if not re.match(r'^\d+$', val):
                            results.append({"Kind": "TemplateParam", "Value": val})
                    # Full template text only with -WithText
                    if include_text:
                        results.append({"Kind": "Template", "Value": text})
                elif include_text:
                    results.append({"Kind": "Text", "Value": text})

    return results


def get_area_cell_data(area, row_map_ref, include_text):
    params = []
    details = []
    texts = []
    templates = []

    start_row = area["BeginRow"]
    end_row = area["EndRow"]
    if start_row == -1:
        start_row = 0
    if end_row == -1:
        end_row = doc_height - 1

    for r in range(start_row, end_row + 1):
        if r in row_map_ref:
            cells = get_cell_data(row_map_ref[r], include_text)
            for c in cells:
                kind = c["Kind"]
                if kind == "Parameter":
                    params.append(c["Value"])
                    if "Detail" in c:
                        details.append(f"{c['Value']}->{c['Detail']}")
                elif kind == "TemplateParam":
                    params.append(f"{c['Value']} [tpl]")
                elif kind == "Text":
                    texts.append(c["Value"])
                elif kind == "Template":
                    templates.append(c["Value"])

    return {"Params": params, "Details": details, "Texts": texts, "Templates": templates}


# Sort areas by position: Rows by beginRow, Columns by beginCol, Rectangle by beginRow
def area_sort_key(a):
    if a["AreaType"] == "Columns":
        return (a["BeginCol"], a["Name"])
    return (a["BeginRow"], a["Name"])

named_areas.sort(key=area_sort_key)

# Collect data for each area
area_data = []
covered_rows = set()

for area in named_areas:
    data = get_area_cell_data(area, row_map, args.WithText)
    area_data.append({
        "Area": area,
        "Params": data["Params"],
        "Details": data["Details"],
        "Texts": data["Texts"],
        "Templates": data["Templates"],
    })

    # Track covered rows
    sr = area["BeginRow"]
    er = area["EndRow"]
    if sr != -1 and er != -1:
        for r in range(sr, er + 1):
            covered_rows.add(r)

# Find parameters outside named areas
outside_params = []
outside_details = []
outside_texts = []
outside_templates = []

for r in sorted(row_map.keys()):
    if r not in covered_rows:
        cells = get_cell_data(row_map[r], args.WithText)
        for c in cells:
            kind = c["Kind"]
            if kind == "Parameter":
                outside_params.append(c["Value"])
                if "Detail" in c:
                    outside_details.append(f"{c['Value']}->{c['Detail']}")
            elif kind == "TemplateParam":
                outside_params.append(f"{c['Value']} [tpl]")
            elif kind == "Text":
                outside_texts.append(c["Value"])
            elif kind == "Template":
                outside_templates.append(c["Value"])

# --- Counts ---
merge_count = len(root.findall("d:merge", NS))
drawing_nodes = root.findall("d:drawing", NS)
drawing_count = len(drawing_nodes)

# --- Output ---

def truncate_list(items, max_count):
    if len(items) <= max_count:
        return ", ".join(items)
    shown = ", ".join(items[:max_count])
    remaining = len(items) - max_count
    return f"{shown}, ... (+{remaining})"


# Determine template name from path
template_name = os.path.basename(os.path.dirname(os.path.dirname(template_path)))

if args.Format == "json":
    result = {
        "name": template_name,
        "rows": doc_height,
        "columns": default_col_count,
        "columnSets": [{"id": cs["Id"], "size": cs["Size"]} for cs in column_sets],
        "areas": [],
        "outsideParams": list(outside_params),
        "mergeCount": merge_count,
        "drawingCount": drawing_count,
    }

    for ad in area_data:
        area_obj = {
            "name": ad["Area"]["Name"],
            "type": ad["Area"]["AreaType"],
            "beginRow": ad["Area"]["BeginRow"],
            "endRow": ad["Area"]["EndRow"],
            "beginCol": ad["Area"]["BeginCol"],
            "endCol": ad["Area"]["EndCol"],
            "params": list(ad["Params"]),
        }
        if ad["Area"]["ColumnsID"]:
            area_obj["columnsID"] = ad["Area"]["ColumnsID"]
        if args.WithText:
            area_obj["texts"] = list(ad["Texts"])
            area_obj["templates"] = list(ad["Templates"])
        result["areas"].append(area_obj)

    if args.WithText:
        result["outsideTexts"] = list(outside_texts)
        result["outsideTemplates"] = list(outside_templates)

    for nd in named_drawings:
        result["areas"].append({
            "name": nd["Name"],
            "type": "Drawing",
            "drawingID": nd["DrawingID"],
        })

    print(json.dumps(result, ensure_ascii=False, indent=2))
    sys.exit(0)

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 "не на поддержке"


# --- Text format output ---
lines = []

lines.append(f"=== {template_name} ===")
_support = get_support_status_for_path(template_path)
if _support is not None:
    lines.append(f"Поддержка: {_support}")
lines.append(f"  Rows: {doc_height}, Columns: {default_col_count}")

if len(column_sets) == 0:
    lines.append("  Column sets: 1 (default only)")
else:
    lines.append(f"  Column sets: {len(column_sets) + 1} (default={default_col_count} cols + {len(column_sets)} additional)")
    for cs in column_sets:
        lines.append(f"    {cs['Id'][:8]}...: {cs['Size']} cols")

lines.append("")
lines.append("--- Named areas ---")

for ad in area_data:
    a = ad["Area"]
    param_count = len(ad["Params"])
    row_range = ""

    if a["AreaType"] == "Rows":
        row_range = f"rows {a['BeginRow']}-{a['EndRow']}"
    elif a["AreaType"] == "Columns":
        row_range = f"cols {a['BeginCol']}-{a['EndCol']}"
    elif a["AreaType"] == "Rectangle":
        row_range = f"rows {a['BeginRow']}-{a['EndRow']}, cols {a['BeginCol']}-{a['EndCol']}"

    cols_info = ""
    if a["ColumnsID"]:
        cs_size = ""
        for cs in column_sets:
            if cs["Id"] == a["ColumnsID"]:
                cs_size = f" {cs['Size']}cols"
                break
        cols_info = f" [colset{cs_size}]"

    param_info = f"({param_count} params)"
    name_str = a["Name"].ljust(25)
    type_str = a["AreaType"].ljust(12)
    lines.append(f"  {name_str} {type_str} {row_range}  {param_info}{cols_info}")

for nd in named_drawings:
    name_str = nd["Name"].ljust(25)
    lines.append(f"  {name_str} Drawing      drawingID={nd['DrawingID']}")

# Detect intersection pairs (Rows + Columns areas that overlap)
rows_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Rows"]
cols_areas = [ad for ad in area_data if ad["Area"]["AreaType"] == "Columns"]
intersections = []
if rows_areas and cols_areas:
    for ra in rows_areas:
        for ca in cols_areas:
            intersections.append(f"{ra['Area']['Name']}|{ca['Area']['Name']}")

if intersections:
    lines.append("")
    lines.append("--- Intersections (use with GetArea) ---")
    for pair in intersections:
        lines.append(f"  {pair}")

# Parameters by area
has_params = any(len(ad["Params"]) > 0 for ad in area_data) or len(outside_params) > 0

if has_params:
    lines.append("")
    lines.append("--- Parameters by area ---")
    for ad in area_data:
        if len(ad["Params"]) > 0:
            param_str = truncate_list(ad["Params"], args.MaxParams)
            lines.append(f"  {ad['Area']['Name']}: {param_str}")
            # Show detailParameters if any
            if len(ad["Details"]) > 0:
                detail_str = truncate_list(ad["Details"], args.MaxParams)
                lines.append(f"    detail: {detail_str}")
    if len(outside_params) > 0:
        param_str = truncate_list(outside_params, args.MaxParams)
        lines.append(f"  (outside areas): {param_str}")
        if len(outside_details) > 0:
            detail_str = truncate_list(outside_details, args.MaxParams)
            lines.append(f"    detail: {detail_str}")

# WithText sections
if args.WithText:
    has_text = any(len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0 for ad in area_data) or len(outside_texts) > 0 or len(outside_templates) > 0

    if has_text:
        lines.append("")
        lines.append("--- Text content ---")
        for ad in area_data:
            if len(ad["Texts"]) > 0 or len(ad["Templates"]) > 0:
                lines.append(f"  {ad['Area']['Name']}:")
                if len(ad["Texts"]) > 0:
                    text_items = [f'"{t}"' for t in ad["Texts"]]
                    text_str = truncate_list(text_items, args.MaxParams)
                    lines.append(f"    Text: {text_str}")
                if len(ad["Templates"]) > 0:
                    tpl_items = [f'"{t}"' for t in ad["Templates"]]
                    tpl_str = truncate_list(tpl_items, args.MaxParams)
                    lines.append(f"    Templates: {tpl_str}")
        if len(outside_texts) > 0 or len(outside_templates) > 0:
            lines.append("  (outside areas):")
            if len(outside_texts) > 0:
                text_items = [f'"{t}"' for t in outside_texts]
                text_str = truncate_list(text_items, args.MaxParams)
                lines.append(f"    Text: {text_str}")
            if len(outside_templates) > 0:
                tpl_items = [f'"{t}"' for t in outside_templates]
                tpl_str = truncate_list(tpl_items, args.MaxParams)
                lines.append(f"    Templates: {tpl_str}")

lines.append("")
lines.append("--- Stats ---")
lines.append(f"  Merges: {merge_count}")
lines.append(f"  Drawings: {drawing_count}")

# --- Truncation protection ---
total_lines = len(lines)

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)
    lines = lines[args.Offset:]

if len(lines) > args.Limit:
    shown = lines[:args.Limit]
    for l in shown:
        print(l)
    remaining = total_lines - args.Offset - args.Limit
    print("")
    print(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.")
else:
    for l in lines:
        print(l)
