"""Builds a JSON hierarchy tree from the Position column of an Excel Stk.-Liste.

Usage:
    python jsonFromXlsx.py --filename Fortna --tab soll
    python jsonFromXlsx.py --filename Fortna --tab ist --output result.json
    python jsonFromXlsx.py --filename KA135776 --extract-ids
    python jsonFromXlsx.py --from-results --extract-ids
"""

import argparse
import json
import os
import re
import sys
from collections import Counter
from pathlib import Path

import openpyxl


SHEET_NAMES = {
    "soll": "Stk.-Liste SOLL",
    "ist": "Stk.-Liste IST",
}

# Column indices (0-based) in the header row
COL_POSITION = 0
COL_TEILENUMMER = 1
COL_BEZEICHNUNG = 2
HEADER_ROW = 3  # 1-based row number of the header


def find_xlsx(examples_dir: str, pattern: str) -> Path:
    """Find an .xlsx file in examples/ whose name contains the given pattern."""
    matches = []
    for f in Path(examples_dir).glob("*.xlsx"):
        if pattern.lower() in f.name.lower():
            matches.append(f)
    if not matches:
        print(f"FEHLER: Keine .xlsx-Datei mit Muster '{pattern}' in {examples_dir} gefunden.")
        sys.exit(1)
    if len(matches) > 1:
        print(f"Mehrere Dateien gefunden fuer Muster '{pattern}':")
        for m in sorted(matches):
            print(f"  {m.name}")
        print(f"Verwende: {matches[0].name}")
    return sorted(matches)[0]


def make_key(teilenummer: str, bezeichnung: str, is_unique: bool) -> str:
    """Build a node key from Teilenummer, adding Bezeichnung if not unique."""
    if is_unique:
        return teilenummer
    combined = f"{teilenummer}-{bezeichnung}"
    return combined.replace(" ", "_")


def unique_key(parent: dict, key: str) -> str:
    """Return key with _1, _2, ... suffix if it already exists in parent."""
    if key not in parent:
        return key
    suffix = 1
    while f"{key}_{suffix}" in parent:
        suffix += 1
    return f"{key}_{suffix}"


def build_tree(ws, sheet_type: str) -> dict:
    """Read the worksheet and build a nested dict from Position paths.

    Keys are Teilenummer-based (with Bezeichnung if non-unique).
    Position paths are used only for hierarchy, not as keys.
    """
    # First pass: collect all Teilenummern to determine uniqueness
    teil_counter: Counter = Counter()
    rows_data = []
    for row in ws.iter_rows(min_row=HEADER_ROW + 1, max_row=ws.max_row, values_only=True):
        pos = row[COL_POSITION]
        teil = row[COL_TEILENUMMER]
        bez = row[COL_BEZEICHNUNG]
        if pos is None or teil is None:
            continue
        pos_str = str(pos).strip()
        if pos_str.lower() == "position":
            continue  # skip header row
        teil_str = str(teil).strip()
        bez_str = str(bez).strip() if bez else ""
        teil_counter[teil_str] += 1
        rows_data.append((pos_str, teil_str, bez_str))

    unique_teile = {t for t, count in teil_counter.items() if count == 1}

    # Second pass: build tree with Teilenummer-based keys
    tree: dict = {}
    pos_to_node: dict[str, dict] = {}  # maps position path -> dict node
    # Track metadata per node for leaf post-processing
    node_meta: dict[int, tuple[str, str]] = {}  # id(child) -> (teil_str, bez_str)

    for pos_str, teil_str, bez_str in rows_data:
        key = make_key(teil_str, bez_str, teil_str in unique_teile)
        segments = pos_str.split("/")

        # Find parent dict
        if len(segments) == 1:
            parent = tree
        else:
            parent_pos = "/".join(segments[:-1])
            if parent_pos not in pos_to_node:
                # Parent row missing in data, create placeholder
                pos_to_node[parent_pos] = {}
            parent = pos_to_node[parent_pos]

        # Insert with unique key
        final_key = unique_key(parent, key)
        child: dict = {}
        parent[final_key] = child
        pos_to_node[pos_str] = child
        node_meta[id(child)] = (teil_str, bez_str)

    is_sivas = re.compile(r"^\d{9}$").match

    # Post-process: leaves (empty dicts) -> key=Teilenummer, value=Bezeichnung
    def simplify_leaves(node: dict) -> dict:
        result: dict = {}
        for key, value in node.items():
            if isinstance(value, dict) and len(value) == 0:
                # Leaf node: use only Teilenummer as key, Bezeichnung as value
                teil, bez = node_meta.get(id(value), ("", ""))
                if teil in result and is_sivas(teil):
                    # Aggregate duplicate 9-digit Sivas numbers
                    existing = result[teil]
                    if isinstance(existing, str):
                        result[teil] = {"anzahl": 2, "beschreibung": existing}
                    else:
                        existing["anzahl"] += 1
                else:
                    leaf_key = unique_key(result, teil)
                    result[leaf_key] = bez
            elif isinstance(value, dict):
                result[key] = simplify_leaves(value)
            else:
                result[key] = value
        return result

    return simplify_leaves(tree)


RE_SIVAS = re.compile(r"^(\d{9})")


def is_leaf_value(v) -> bool:
    """True if v is a leaf entry (string or anzahl-dict)."""
    if isinstance(v, str):
        return True
    if isinstance(v, dict) and "anzahl" in v:
        return True
    return False


def extract_ids_from_tree(tree: dict) -> dict[str, str]:
    """Collect 9-digit Sivas numbers one level above leaves.

    Returns dict of nummer -> bezeichnung.
    """
    ids: dict[str, str] = {}

    def walk(node: dict):
        for key, value in node.items():
            if not isinstance(value, dict) or "anzahl" in value:
                continue  # leaf, skip
            # Check if all children are leaves
            all_leaves = all(is_leaf_value(v) for v in value.values())
            m = RE_SIVAS.match(key)
            if all_leaves and value and m:
                nummer = m.group(1)
                # Bezeichnung from key: part after "nummer-", underscores back to spaces
                if "-" in key[9:]:
                    bez = key[10:].replace("_", " ")
                elif len(key) > 9:
                    bez = key[10:].replace("_", " ")
                else:
                    bez = nummer
                ids[nummer] = bez
            else:
                walk(value)

    walk(tree)
    return ids


def process_file(xlsx_path: Path, sheet_name: str, tab: str, results_dir: Path,
                  out_path: Path | None = None) -> bool:
    """Process a single xlsx file. Returns True on success."""
    print(f"Lade: {xlsx_path.name}")
    print(f"Sheet: {sheet_name}")

    wb = openpyxl.load_workbook(str(xlsx_path), data_only=True)
    if sheet_name not in wb.sheetnames:
        print(f"  SKIP: Sheet '{sheet_name}' nicht vorhanden.")
        return False

    ws = wb[sheet_name]
    tree = build_tree(ws, tab)

    if out_path is None:
        stem = xlsx_path.stem.replace(" ", "_")
        out_path = results_dir / f"{stem}_{tab}.json"

    with open(out_path, "w", encoding="utf-8") as f:
        json.dump(tree, f, indent=2, ensure_ascii=False)

    print(f"  Ausgabe: {out_path}")
    return True


def main():
    parser = argparse.ArgumentParser(
        description="Erzeugt eine JSON-Baumstruktur aus der Positionsspalte einer Excel-Stueckliste."
    )
    parser.add_argument(
        "--filename",
        default=None,
        help="Suchmuster fuer die .xlsx-Datei im examples/-Ordner (z.B. 'Fortna')",
    )
    parser.add_argument(
        "--tab",
        choices=["soll", "ist"],
        default="soll",
        help="Welches Tabellenblatt gelesen wird: 'soll' oder 'ist' (default: soll)",
    )
    parser.add_argument(
        "--output",
        default=None,
        help="Ausgabedatei (default: <filename>_<tab>.json im results/-Ordner)",
    )
    parser.add_argument(
        "--all",
        action="store_true",
        dest="process_all",
        help="Alle .xlsx-Dateien im examples/-Ordner in _ist und _soll JSON konvertieren",
    )
    parser.add_argument(
        "--extract-ids",
        action="store_true",
        dest="extract_ids",
        help="9-stellige Sivas-Nummern eine Ebene ueber den Blaettern extrahieren",
    )
    parser.add_argument(
        "--from-results",
        action="store_true",
        dest="from_results",
        help="JSON-Dateien aus results/ lesen statt aus .xlsx zu erzeugen",
    )
    args = parser.parse_args()

    if not args.process_all and not args.filename and not args.from_results:
        parser.error("Entweder --filename, --all oder --from-results muss angegeben werden.")

    # Resolve paths relative to project root
    project_root = Path(os.environ.get("PROJECT", Path(__file__).resolve().parent.parent))
    examples_dir = project_root / "examples"
    results_dir = project_root / "results"
    results_dir.mkdir(exist_ok=True)

    # Default tab to "ist" when --extract-ids without explicit --tab
    tab = args.tab
    if args.extract_ids and args.tab == "soll" and "--tab" not in sys.argv:
        tab = "ist"

    if args.from_results:
        # Read existing JSON files from results/
        pattern = f"*_{tab}.json"
        json_files = sorted(results_dir.glob(pattern))
        if args.filename:
            json_files = [f for f in json_files if args.filename.lower() in f.name.lower()]
        if not json_files:
            print(f"FEHLER: Keine JSON-Dateien mit Muster '{pattern}' in {results_dir} gefunden.")
            sys.exit(1)
        print(f"Lese {len(json_files)} JSON-Dateien aus {results_dir}\n")
        for json_path in json_files:
            with open(json_path, encoding="utf-8") as f:
                tree = json.load(f)
            # Derive project name from filename: strip _ist/_soll.json suffix
            project_name = json_path.stem
            for suffix in ("_ist", "_soll"):
                if project_name.endswith(suffix):
                    project_name = project_name[: -len(suffix)]
                    break
            if args.extract_ids:
                ids = extract_ids_from_tree(tree)
                ids_path = results_dir / f"{project_name}_ids_{tab}.txt"
                with open(ids_path, "w", encoding="utf-8") as f:
                    for nummer, bez in sorted(ids.items()):
                        f.write(f"{nummer}: {bez}\n")
                print(f"  {json_path.name} -> {ids_path.name} ({len(ids)} IDs)")
    elif args.process_all:
        xlsx_files = sorted(f for f in Path(examples_dir).glob("*.xlsx")
                            if not f.name.startswith("~$"))
        print(f"Verarbeite {len(xlsx_files)} Dateien aus {examples_dir}\n")
        ok, skip = 0, 0
        for xlsx_path in xlsx_files:
            for t in ("soll", "ist"):
                sheet_name = SHEET_NAMES[t]
                if process_file(xlsx_path, sheet_name, t, results_dir):
                    ok += 1
                else:
                    skip += 1
            print()
        print(f"Fertig: {ok} JSON erzeugt, {skip} uebersprungen.")
    else:
        xlsx_path = find_xlsx(str(examples_dir), args.filename)
        sheet_name = SHEET_NAMES[tab]

        out_path = None
        if args.output:
            out_path = Path(args.output)
            if not out_path.is_absolute():
                out_path = results_dir / out_path

        json_out = None
        if args.extract_ids:
            stem = xlsx_path.stem.replace(" ", "_")
            json_out = results_dir / f"{stem}_{tab}.json"

        if not process_file(xlsx_path, sheet_name, tab, results_dir, out_path):
            sys.exit(1)

        if args.extract_ids:
            # Read back the generated JSON
            read_path = out_path or json_out or results_dir / f"{xlsx_path.stem.replace(' ', '_')}_{tab}.json"
            with open(read_path, encoding="utf-8") as f:
                tree = json.load(f)
            ids = extract_ids_from_tree(tree)
            project_name = xlsx_path.stem.replace(" ", "_")
            ids_path = results_dir / f"{project_name}_ids_{tab}.txt"
            with open(ids_path, "w", encoding="utf-8") as f:
                for nummer, bez in sorted(ids.items()):
                    f.write(f"{nummer}: {bez}\n")
            print(f"  IDs: {ids_path} ({len(ids)} IDs)")


if __name__ == "__main__":
    main()
