"""
init-mater-project

Copyright (C) 2025 [Lauranne Sarribouette] <lauranne.sarribouette@univ-grenoble-alpes.fr>

SPDX-License-Identifier: LGPL-3.0-or-later
"""

import json
from pathlib import Path
from typing import List, Dict, Optional, Tuple


def load_mapping_file(mapping_file: Path) -> list[dict]:
    """Load dimensions mapping from JSON file"""
    if not mapping_file.exists():
        raise FileNotFoundError(f"Mapping file not found: {mapping_file}")

    with open(mapping_file, "r", encoding="utf-8") as f:
        data = json.load(f)

    if not isinstance(data, list):
        raise ValueError(f"Mapping file must contain an array, got: {type(data)}")

    return data


def combine_dimensions(
    local_dimensions: list[dict], ref_dimensions: list[dict]
) -> list[dict]:
    """Combine local dimensions with reference, handling equivalences and parent merging"""

    # Step 1: Deduplicate and merge local entries with same (name, value)
    merged_local_dimensions = deduplicate_input(local_dimensions)

    # Step 2: Index references
    ref_by_value: dict[str, dict[str, dict]] = {}
    ref_by_equiv: dict[str, dict[str, dict]] = {}

    for ref in ref_dimensions:
        name = ref["name"]
        value = ref["value"]

        if name not in ref_by_value:
            ref_by_value[name] = {}
        ref_by_value[name][value] = ref

        for equiv_value in ref.get("equivalence", {}).values():
            if name not in ref_by_equiv:
                ref_by_equiv[name] = {}
            ref_by_equiv[name][equiv_value] = ref

    # Step 3: Process local dimensions and track which references were used
    combined: list[dict] = []
    used_refs: set[tuple[str, str]] = set()
    stats = {
        "total": 0,
        "found_as_value": 0,
        "found_as_equivalence": 0,
        "not_found": 0,
        "parents_added": 0,
    }

    for entry in merged_local_dimensions:
        name = entry["name"]
        value = entry["value"]

        stats["total"] += 1
        ref = None
        match_info = None

        # Check if value exists in reference
        if name in ref_by_value and value in ref_by_value[name]:
            ref = ref_by_value[name][value]
            match_info = ("value", None)
            stats["found_as_value"] += 1
            used_refs.add((name, value))

        # Check if value exists as equivalence in reference
        elif name in ref_by_equiv and value in ref_by_equiv[name]:
            ref = ref_by_equiv[name][value]
            match_info = ("equivalence", value)
            stats["found_as_equivalence"] += 1
            used_refs.add((ref["name"], ref["value"]))

        # Check if local equivalences point to reference
        else:
            for equiv_value in entry.get("equivalence", {}).values():
                if name in ref_by_value and equiv_value in ref_by_value[name]:
                    ref = ref_by_value[name][equiv_value]
                    match_info = ("local_equiv", value)
                    stats["found_as_equivalence"] += 1
                    used_refs.add((ref["name"], ref["value"]))
                    break
                elif name in ref_by_equiv and equiv_value in ref_by_equiv[name]:
                    ref = ref_by_equiv[name][equiv_value]
                    match_info = ("local_equiv", value)
                    stats["found_as_equivalence"] += 1
                    used_refs.add((ref["name"], ref["value"]))
                    break

        # Merge or keep as is
        if ref:
            result, parents_added = merge_with_reference(entry, ref, match_info)
            if parents_added:
                stats["parents_added"] += 1
        else:
            result = entry.copy()
            stats["not_found"] += 1

        combined.append(result)

    # Step 4: Add unused references
    unused_count = 0
    for ref in ref_dimensions:
        ref_key = (ref["name"], ref["value"])
        if ref_key not in used_refs:
            combined.append(ref.copy())
            unused_count += 1

    # Print summary
    print(f"✅ Processed {stats['total']} local dimensions:")
    print(f"   • {stats['found_as_value']} matched by value")
    print(f"   • {stats['found_as_equivalence']} matched by equivalence")
    print(f"   • {stats['not_found']} kept as-is (not in reference)")
    if stats["parents_added"] > 0:
        print(f"   ℹ️  {stats['parents_added']} with additional parent added")
    print(f"   • {unused_count} reference dimensions added")
    print(f"\n📊 Total project dimensions: {len(combined)}")

    return combined


def deduplicate_input(input_data: list[dict]) -> list[dict]:
    """Merge input entries with same (name, value) but different parents"""

    grouped: dict[tuple[str, str], list[dict]] = {}

    for entry in input_data:
        name = entry["name"]
        value = entry["value"]
        key = (name, value)

        if key not in grouped:
            grouped[key] = []
        grouped[key].append(entry)

    merged: list[dict] = []

    for entries in grouped.values():
        if len(entries) == 1:
            # Ensure "default" exists even for single entries
            result = entries[0].copy()
            result["parents_values"] = ensure_default_parent(
                result.get("parents_values", {})
            )
            merged.append(result)
        else:
            # Merge multiple entries
            result = {
                "name": entries[0]["name"],
                "value": entries[0]["value"],
                "equivalence": {},
                "parents_values": {},
            }

            # Merge equivalences
            for entry in entries:
                result["equivalence"].update(entry.get("equivalence", {}))

            # Merge parents: preserve named keys, renumber "default" conflicts
            all_parents_values: dict[str, str] = {}
            default_parents: list[str] = []

            for entry in entries:
                parents = entry.get("parents_values", {})
                for key, value in parents.items():
                    if key == "default":
                        if value not in default_parents:
                            default_parents.append(value)
                    else:
                        # Named key - preserve as-is if not duplicate
                        if key not in all_parents_values:
                            all_parents_values[key] = value

            # Add default parents (first as "default", rest as "hierarchy_N")
            if default_parents:
                all_parents_values["default"] = default_parents[0]

                # Find next available hierarchy_N number
                existing_nums = [
                    int(k.split("_")[1])
                    for k in all_parents_values.keys()
                    if k.startswith("hierarchy_") and k.split("_")[1].isdigit()
                ]
                next_num = max(existing_nums, default=0) + 1

                for parent in default_parents[1:]:
                    all_parents_values[f"hierarchy_{next_num}"] = parent
                    next_num += 1

            # If no "default" but other keys exist, create one
            result["parents_values"] = ensure_default_parent(all_parents_values)
            merged.append(result)

    return merged


def ensure_default_parent(parents_values: dict[str, str]) -> dict[str, str]:
    """
    Ensure parents_values has a "default" key by copying first available value.

    Args:
        parents_values: Dictionary of parent hierarchies

    Returns:
        Dictionary with guaranteed "default" key (if any parents exist)
    """
    if not parents_values:
        return {}

    if "default" in parents_values:
        return parents_values

    # Copy first value as "default" (keep original key too)
    result = parents_values.copy()
    first_value = next(iter(parents_values.values()))
    result["default"] = first_value

    return result


def merge_with_reference(
    entry: dict, ref: dict, match_info: tuple[str, str | None]
) -> tuple[dict, bool]:
    """Merge entry with reference, handling parent additions"""

    match_type, original_value = match_info

    result = {
        "name": ref["name"],
        "value": ref["value"],
        "equivalence": ref.get("equivalence", {}).copy(),
    }

    # Add original value as equivalence if not already present
    if match_type in ("equivalence", "local_equiv") and original_value:
        # Check if value already exists in equivalences
        existing_equiv_values = set(result["equivalence"].values())

        if (
            original_value not in existing_equiv_values
            and original_value != ref["value"]
        ):
            # Find next available equiv_N key
            existing_equiv_nums = [
                int(k.split("_")[1])
                for k in result["equivalence"].keys()
                if k.startswith("equiv_") and k.split("_")[1].isdigit()
            ]
            next_num = max(existing_equiv_nums, default=0) + 1
            result["equivalence"][f"equiv_{next_num}"] = original_value

    # Merge parent values - preserve reference keys, add missing entry parents
    ref_parents = ref.get("parents_values", {})
    entry_parents = entry.get("parents_values", {})

    # Start with all reference parents (unchanged)
    merged_parents = ref_parents.copy()

    # Collect all existing parent values (from ref)
    existing_parent_values = set(ref_parents.values())

    # Add entry parents that don't exist yet
    parents_to_add: list[str] = []
    for parent_value in entry_parents.values():
        if parent_value not in existing_parent_values:
            parents_to_add.append(parent_value)

    # Number new parents as hierarchy_N
    if parents_to_add:
        # Find next available hierarchy_N number
        existing_hierarchy_nums = [
            int(k.split("_")[1])
            for k in merged_parents.keys()
            if k.startswith("hierarchy_") and k.split("_")[1].isdigit()
        ]
        next_num = max(existing_hierarchy_nums, default=0) + 1

        for parent in parents_to_add:
            merged_parents[f"hierarchy_{next_num}"] = parent
            next_num += 1

    result["parents_values"] = merged_parents
    parents_added = len(parents_to_add) > 0

    return result, parents_added


def create_dimensions_mapping(
    source_dimensions: List[Dict], reference_dimensions: List[Dict]
) -> List[Dict]:
    """
    # Create mapping by matching source dimensions with reference data

    ## Arguments
    - `source_dimensions` (List[Dict]): Unique dimensions from input data
    - `reference_dimensions` (List[Dict]): Reference dimensions with hierarchies

    ## Returns
    - `List[Dict]`: Mapping with matched and TODO entries
    """
    reference_index = create_reference_indexes(reference_dimensions)
    mapping = []

    for dimension in source_dimensions:
        name, value = dimension["name"], dimension["value"]

        reference_entry = find_dimension_in_reference(name, value, reference_index)

        if reference_entry:
            parent_value = get_parent_value(reference_entry)
            mapping_entry = {
                "name": reference_entry.get("name", ""),
                "value": reference_entry.get("value", ""),
                "equivalence": reference_entry.get("equivalence", {}),
                "parents_values": {"default": parent_value} if parent_value else {},
            }
        else:
            mapping_entry = {
                "name": name,
                "value": value,
                "equivalence": {
                    "TODO": "check in 'data/references/dimensions.json' if this value exists under another name in reference"
                },
                "parents_values": {
                    "default": "parent_value",
                    "TODO": [
                        "if you found a reference equivalence, leave parents_values empty '{}' to use the equivalence parent value",
                        "or choose a parent value from input data or reference using 'data/references/dimensions.json'",
                    ],
                },
            }

        mapping.append(mapping_entry)

    return mapping


def resolve_single_mapping_entry(
    entry: Dict, reference_index: Dict, all_input_dimensions: List[Dict]
) -> Tuple[Dict, List[str], bool]:
    """
    # Resolve single mapping entry references and validate

    ## Arguments
    - `entry` (Dict): Single mapping entry to resolve
    - `reference_index` (Dict): Reference lookup indexes
    - `all_input_dimensions` (List[Dict]): All available input dimensions

    ## Returns
    - `Tuple[Dict, List[str], bool]`: (resolved_entry, errors, was_auto_resolved)
    """
    name = entry["name"]
    value = entry["value"]
    equivalence = entry.get("equivalence", {})
    parents_values = entry.get("parents_values", {})
    errors = []
    auto_resolved = False

    resolved_entry = entry.copy()

    if "TODO" in equivalence or "TODO" in parents_values:
        errors.append(f"{name}={value}: Contains TODO keys, manual completion required")
        return resolved_entry, errors, auto_resolved

    hierarchy_keys = list(parents_values.keys())
    if len(hierarchy_keys) > 1:
        errors.append(
            f"{name}={value}: parents_values must contain only one value, got: {hierarchy_keys}"
        )
        return resolved_entry, errors, auto_resolved

    parent_value = next(iter(parents_values.values())) if parents_values else None

    if equivalence:
        ref_value = list(equivalence.values())[0] if equivalence else None
        if ref_value:
            ref_entry = find_dimension_in_reference(name, ref_value, reference_index)
            if not ref_entry:
                errors.append(
                    f"{name}={value}: equivalence '{ref_value}' not found in reference"
                )
            else:
                if not parents_values:
                    ref_parent = get_parent_value(ref_entry)
                    if ref_parent:
                        resolved_entry["parents_values"] = {"default": ref_parent}
                        auto_resolved = True
                        parent_value = ref_parent

    if parent_value:
        parent_exists = find_dimension_in_reference(
            name, parent_value, reference_index
        ) is not None or any(
            dim["name"] == name and dim["value"] == parent_value
            for dim in all_input_dimensions
        )

        if not parent_exists:
            errors.append(
                f"{name}={value}: parent '{parent_value}' not found in reference or input data"
            )

    return resolved_entry, errors, auto_resolved


def create_reference_indexes(reference_dimensions: List[Dict]) -> Dict:
    """
    # Create lookup indexes for reference dimensions (direct + equivalence, case-insensitive)

    ## Arguments
    - `reference_dimensions` (List[Dict]): Reference dimensions data

    ## Returns
    - `Dict`: Dictionary with 'direct' and 'equivalence' indexes
    """
    direct_index = {}
    equivalence_index = {}

    for entry in reference_dimensions:
        name = entry.get("name")
        value = entry.get("value")

        if not name or not value:
            continue

        # Direct index (lowercase for case-insensitive matching)
        direct_index[(name, value.lower())] = entry

        # Equivalence index (lowercase for case-insensitive matching)
        equivalences = entry.get("equivalence", {})
        for equiv_key, equiv_value in equivalences.items():
            if isinstance(equiv_value, str):
                equivalence_index[(name, equiv_value.lower())] = entry

    return {"direct": direct_index, "equivalence": equivalence_index}


def find_dimension_in_reference(
    name: str, value: str, reference_index: Dict
) -> Optional[Dict]:
    """
    # Find dimension in reference by exact match or equivalence lookup (case-insensitive)

    ## Arguments
    - `name` (str): Dimension name to search
    - `value` (str): Dimension value to search
    - `reference_index` (Dict): Pre-built reference indexes

    ## Returns
    - `Optional[Dict]`: Reference entry if found, None otherwise
    """
    if not reference_index:
        return None

    direct_index, equivalence_index = (
        reference_index["direct"],
        reference_index["equivalence"],
    )
    value_lower = value.lower()

    if (name, value_lower) in direct_index:
        return direct_index[(name, value_lower)]

    if (name, value_lower) in equivalence_index:
        return equivalence_index[(name, value_lower)]

    return None


def get_parent_value(entry: Dict) -> Optional[str]:
    """
    # Get parent value from reference entry using 'parents_values' field

    ## Arguments
    - `entry` (Dict): Reference entry with parents_values

    ## Returns
    - `Optional[str]`: Parent value if found, None otherwise
    """
    parents_values = entry.get("parents_values", {})
    return parents_values.get("default")


def build_complete_hierarchy_from_mapping(mapping: List[Dict]) -> List[Dict]:
    """
    # Build complete hierarchy by resolving parents and creating missing parents

    ## Arguments
    - `mapping` (List[Dict]): Dimensions mapping with parent relationships

    ## Returns
    - `List[Dict]`: Complete hierarchy with resolved parents and missing parents
    """
    complete_hierarchy = []
    processed_combinations = set()
    reference_dimensions = []
    reference_index = {}

    reference_path = Path("data/dimensions/project_dimensions.json")
    if reference_path.exists():
        reference_dimensions = load_reference_dimensions_data(reference_path)
        reference_index = create_reference_indexes(reference_dimensions)

    for entry in mapping:
        name, value = entry["name"], entry["value"]
        combination = (name, value)

        if combination in processed_combinations:
            continue

        resolved_entry = resolve_parent_from_reference(entry, reference_index)
        complete_hierarchy.append(resolved_entry)
        processed_combinations.add(combination)

    missing_parents = find_and_create_missing_parents(
        complete_hierarchy, reference_index
    )
    for parent_entry in missing_parents:
        parent_combination = (parent_entry["name"], parent_entry["value"])
        if parent_combination not in processed_combinations:
            complete_hierarchy.append(parent_entry)
            processed_combinations.add(parent_combination)

    return complete_hierarchy


def resolve_parent_from_reference(mapping_entry: Dict, reference_index: Dict) -> Dict:
    """
    # Resolve parent hierarchy using equivalence logic

    ## Arguments
    - `mapping_entry` (Dict): Entry from mapping file
    - `reference_index` (Dict): Reference lookup indexes

    ## Returns
    - `Dict`: Entry with resolved parent hierarchy
    """
    name = mapping_entry["name"]
    value = mapping_entry["value"]
    equivalence = mapping_entry.get("equivalence", {})
    parents_values = mapping_entry.get("parents_values", {})

    if "TODO" in equivalence or "TODO" in parents_values:
        raise ValueError(f"Cannot build entry with TODO keys: {name}={value}")

    resolved_entry = {
        "name": name,
        "value": value,
        "equivalence": equivalence.copy(),
        "parents_values": parents_values.copy(),
    }

    if equivalence:
        ref_value = list(equivalence.values())[0] if equivalence else None
        if ref_value:
            ref_entry = find_dimension_in_reference(name, ref_value, reference_index)
            if ref_entry:
                if not parents_values or not parents_values.get("default"):
                    ref_parent = get_parent_value(ref_entry)
                    if ref_parent:
                        resolved_entry["parents_values"] = {"default": ref_parent}
                    else:
                        resolved_entry["parents_values"] = {}

    return resolved_entry


def find_and_create_missing_parents(
    hierarchy_entries: List[Dict], reference_index: Dict
) -> List[Dict]:
    """
    # Find and create missing parent entries recursively

    ## Arguments
    - `hierarchy_entries` (List[Dict]): Current hierarchy entries
    - `reference_index` (Dict): Reference lookup indexes

    ## Returns
    - `List[Dict]`: Missing parent entries to add
    """
    existing_values_by_name = {}
    missing_parents = []
    parents_to_process = []

    for entry in hierarchy_entries:
        name = entry["name"]
        value = entry["value"]
        if name not in existing_values_by_name:
            existing_values_by_name[name] = set()
        existing_values_by_name[name].add(value)

    for entry in hierarchy_entries:
        name = entry["name"]
        parent_value = entry.get("parents_values", {}).get("default")
        if parent_value:
            if (
                name not in existing_values_by_name
                or parent_value not in existing_values_by_name[name]
            ):
                parents_to_process.append((name, parent_value))

    while parents_to_process:
        name, parent_value = parents_to_process.pop(0)

        if (
            name in existing_values_by_name
            and parent_value in existing_values_by_name[name]
        ):
            continue

        parent_entry = create_parent_entry(name, parent_value, reference_index)
        missing_parents.append(parent_entry)

        if name not in existing_values_by_name:
            existing_values_by_name[name] = set()
        existing_values_by_name[name].add(parent_value)

        grandparent_value = parent_entry.get("parents_values", {}).get("default")
        if grandparent_value:
            if (
                name not in existing_values_by_name
                or grandparent_value not in existing_values_by_name[name]
            ):
                parents_to_process.append((name, grandparent_value))

    return missing_parents


def create_parent_entry(name: str, value: str, reference_index: Dict) -> Dict:
    """
    # Create parent entry, using reference if available

    ## Arguments
    - `name` (str): Dimension name
    - `value` (str): Dimension value
    - `reference_index` (Dict): Reference lookup indexes

    ## Returns
    - `Dict`: Parent entry with proper format
    """
    ref_entry = find_dimension_in_reference(name, value, reference_index)

    if ref_entry:
        ref_parent = get_parent_value(ref_entry)
        return {
            "name": name,
            "value": value,
            "equivalence": ref_entry.get("equivalence", {}),
            "parents_values": {"default": ref_parent} if ref_parent else {},
        }
    else:
        return {
            "name": name,
            "value": value,
            "equivalence": {},
            "parents_values": {},
        }


def build_hierarchy_tree(entries: List[Dict]) -> Dict:
    """
    # Build tree structure from flat dimension entries

    ## Arguments
    - `entries` (List[Dict]): Dimension entries for same dimension name

    ## Returns
    - `Dict`: Tree structure with parent-child relationships
    """
    entries_by_value = {entry["value"]: entry for entry in entries}

    root_nodes = []
    for entry in entries:
        parent = get_parent_value(entry)
        if not parent:
            root_nodes.append(entry["value"])

    tree = {}
    for root in root_nodes:
        if root in entries_by_value:
            tree[root] = build_subtree(root, entries_by_value)

    return tree


def build_subtree(parent_value: str, entries_by_value: Dict) -> Dict:
    """
    # Recursively build subtree for a parent node

    ## Arguments
    - `parent_value` (str): Parent node value
    - `entries_by_value` (Dict): Lookup dictionary of all entries

    ## Returns
    - `Dict`: Subtree structure
    """
    subtree = {"entry": entries_by_value.get(parent_value, {}), "children": {}}

    for value, entry in entries_by_value.items():
        entry_parent = get_parent_value(entry)
        if entry_parent == parent_value:
            subtree["children"][value] = build_subtree(value, entries_by_value)

    return subtree


def load_reference_dimensions_data(reference_path: Path) -> List[Dict]:
    """
    # Load reference dimensions from JSON file (internal utility)

    ## Arguments
    - `reference_path` (Path): Path to reference dimensions JSON file

    ## Returns
    - `List[Dict]`: Reference dimensions data
    """
    import json

    if not reference_path.exists():
        return []

    try:
        with open(reference_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        if not isinstance(data, list):
            return []

        return data

    except json.JSONDecodeError:
        return []


def normalize_dimension_name(name: str) -> str:
    """
    # Normalize dimension name to base category

    ## Arguments
    - `name` (str): Dimension name (base or derived with underscore)

    ## Returns
    - `str`: Base dimension name (location, object, or process)
    """
    base_names = {"location", "object", "process"}

    if name in base_names:
        return name

    for base in base_names:
        if name.startswith(f"{base}_"):
            return base

    return name
