"""Template context builders for the build pipeline.

Takes reader output and semantics IR, and produces dicts suitable
for rendering Jinja2 templates that generate code, tests, and docs.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

from byteb4rb1e.sphinxcontrib.h5p.utils.pkg.semantics.ir import (
    DirectiveIR,
    LibraryChildIR,
    OptionIR,
    StructuralChildIR,
)

from .naming import (
    directive_to_directive_class,
    directive_to_node_class,
    directive_to_snake,
    option_to_test_name,
)
from .readers import LibraryInfo


@dataclass(frozen=True)
class OptionMapping:
    """Maps an RST option to its H5P JSON param path."""

    option_name: str
    field_name: str
    field_type: str
    param_path: Tuple[str, ...]
    is_negated: bool


# -- option_spec type strings ------------------------------------------

_TYPE_TO_VALIDATOR = {
    "text": "directives.unchanged",
    "number": "directives.unchanged",
}


def _option_spec_entries(
    options: Tuple[OptionIR, ...],
) -> List[Dict[str, str]]:
    """Build option_spec entries for a directive.

    Boolean options produce two entries: one flag (sets True) and
    one ``no-`` prefixed flag (sets False).  Other types map to
    their validator string.
    """
    entries = []
    for opt in options:
        if opt.field_type == "boolean":
            entries.append({
                "name": opt.name,
                "validator": "directives.flag",
            })
            entries.append({
                "name": "no-%s" % opt.name,
                "validator": "directives.flag",
            })
        elif opt.field_type == "select":
            entries.append({
                "name": opt.name,
                "validator": "validate_%s" % option_to_test_name(
                    opt.name,
                ),
            })
        else:
            validator = _TYPE_TO_VALIDATOR.get(
                opt.field_type, "directives.unchanged",
            )
            entries.append({
                "name": opt.name,
                "validator": validator,
            })
    return entries


def _select_validators(
    options: Tuple[OptionIR, ...],
) -> List[Dict[str, Any]]:
    """Build validator function definitions for select options."""
    validators = []
    for opt in options:
        if opt.field_type != "select":
            continue
        func_name = "validate_%s" % option_to_test_name(opt.name)
        allowed = [c[0] for c in opt.choices]
        validators.append({
            "func_name": func_name,
            "option_name": opt.name,
            "allowed_values": allowed,
        })
    return validators


# -- Context builders --------------------------------------------------

def assets_context(
    vendor_name: str,
    lib_info: LibraryInfo,
    import_name: str,
) -> Dict[str, Any]:
    """Build context for asset registration templates.

    Used by ``_generated/__init__.py.j2``.
    """
    return {
        "machine_name": lib_info.machine_name,
        "vendor_name": vendor_name,
        "import_name": import_name,
        "js_files": list(lib_info.preloaded_js),
        "css_files": list(lib_info.preloaded_css),
        "runnable": lib_info.runnable,
    }


def _child_context(
    child: StructuralChildIR,
) -> Dict[str, Any]:
    """Build context for a single structural child directive."""
    child_entries = _option_spec_entries(child.options)
    child_validators = _select_validators(child.options)
    ctx = {
        "directive_name": child.directive_name,
        "node_class": directive_to_node_class(
            child.directive_name,
        ),
        "directive_class": directive_to_directive_class(
            child.directive_name,
        ),
        "snake_name": directive_to_snake(child.directive_name),
        "entity": child.entity,
        "min_count": child.min_count,
        "max_count": child.max_count,
        "has_body": child.body is not None,
        "option_spec": child_entries,
        "select_validators": child_validators,
    }
    if child.body is not None:
        ctx["body_field_name"] = child.body.field_name
        ctx["body_field_type"] = child.body.field_type
    return ctx


def _library_child_context(
    child: LibraryChildIR,
) -> Dict[str, Any]:
    """Build context for a single library child directive."""
    return {
        "directive_name": child.directive_name,
        "node_class": directive_to_node_class(
            child.directive_name,
        ),
        "directive_class": directive_to_directive_class(
            child.directive_name,
        ),
        "snake_name": directive_to_snake(child.directive_name),
        "entity": child.entity,
        "libraries": list(child.libraries),
    }


def directive_context(
    ir: DirectiveIR,
    lib_info: LibraryInfo,
) -> Dict[str, Any]:
    """Build context for directive template.

    Used by ``_generated/directive.py.j2``.  Generates option_spec
    entries, node class definitions, boolean flag+no- pairs, and
    structural/library child contexts.
    """
    entries = _option_spec_entries(ir.options)
    validators = _select_validators(ir.options)

    structural = [
        _child_context(c) for c in ir.structural_children
    ]
    library = [
        _library_child_context(c)
        for c in ir.library_children
    ]

    return {
        "machine_name": ir.machine_name,
        "directive_name": ir.directive_name,
        "library": ir.library,
        "node_class": directive_to_node_class(ir.directive_name),
        "directive_class": directive_to_directive_class(
            ir.directive_name,
        ),
        "snake_name": directive_to_snake(ir.directive_name),
        "has_body": ir.body is not None,
        "body_field_name": (
            ir.body.field_name if ir.body else None
        ),
        "body_field_type": (
            ir.body.field_type if ir.body else None
        ),
        "option_spec": entries,
        "select_validators": validators,
        "structural_children": structural,
        "library_children": library,
        "runnable": lib_info.runnable,
        "major_version": lib_info.major_version,
        "minor_version": lib_info.minor_version,
    }


def _build_option_mappings(
    options: Tuple[OptionIR, ...],
    semantics: List[Dict[str, Any]],
) -> List[OptionMapping]:
    """Map flattened RST options back to nested H5P JSON paths.

    Reconstructs group membership from the original semantics
    fields.  The classifier flattens behaviour fields without
    prefix and other groups with their group name as prefix.
    """
    # Build a lookup: group_name → set of field names
    group_fields = _collect_group_fields(semantics)

    mappings = []
    for opt in options:
        path, is_negated = _resolve_param_path(
            opt, group_fields,
        )
        mappings.append(OptionMapping(
            option_name=opt.name,
            field_name=opt.field_name,
            field_type=opt.field_type,
            param_path=tuple(path),
            is_negated=is_negated,
        ))

        # Boolean options also get a negated mapping
        if opt.field_type == "boolean":
            mappings.append(OptionMapping(
                option_name="no-%s" % opt.name,
                field_name=opt.field_name,
                field_type=opt.field_type,
                param_path=tuple(path),
                is_negated=True,
            ))

    return mappings


def _collect_group_fields(
    semantics: List[Dict[str, Any]],
    prefix: str = "",
) -> Dict[str, Dict[str, List[str]]]:
    """Recursively collect group → field name mappings.

    Returns a dict mapping group paths to their info:
    ``{group_path: {"camel_name": camelCase, "fields": [field_names]}}``.
    """
    groups = {}  # type: Dict[str, Dict[str, List[str]]]
    for field in semantics:
        if field.get("type") != "group":
            continue
        name = field.get("name", "")
        is_behaviour = name == "behaviour"
        if is_behaviour:
            group_path = ""
        elif prefix:
            group_path = prefix + "-" + _camel_to_kebab(name)
        else:
            group_path = _camel_to_kebab(name)

        child_names = []
        for child in field.get("fields", []):
            child_names.append(child.get("name", ""))
            # Recurse into nested groups
            if child.get("type") == "group":
                nested = _collect_group_fields(
                    [child],
                    prefix=group_path,
                )
                groups.update(nested)
        groups[group_path] = {
            "camel_name": name,
            "fields": child_names,
        }
    return groups


def _camel_to_kebab(s: str) -> str:
    """Quick camelCase to kebab-case for path building."""
    import re
    s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1-\2", s)
    s = re.sub(r"([a-z])([A-Z])", r"\1-\2", s)
    return s.lower()


def _resolve_param_path(
    opt: OptionIR,
    group_fields: Dict[str, Dict[str, List[str]]],
) -> Tuple[List[str], bool]:
    """Determine the nested JSON path for an option.

    Returns ``(path_segments, is_negated)`` where is_negated is
    always False for the base option (negated entries are added
    separately).
    """
    # Check behaviour group (path "")
    if "" in group_fields:
        if opt.field_name in group_fields[""]["fields"]:
            return ["behaviour", opt.field_name], False

    # Check named groups — the option name starts with the group prefix
    for group_path, info in group_fields.items():
        if not group_path:
            continue
        if opt.field_name in info["fields"]:
            # Reconstruct the camelCase path segments
            path = _group_path_to_camel_segments(
                group_path, info["camel_name"],
                group_fields,
            )
            path.append(opt.field_name)
            return path, False

    # Top-level field
    return [opt.field_name], False


def _group_path_to_camel_segments(
    group_path: str,
    camel_name: str,
    group_fields: Dict[str, Dict[str, List[str]]],
) -> List[str]:
    """Convert a kebab group path back to camelCase path segments."""
    # For a simple (non-nested) group, just return its camelCase name
    parts = group_path.split("-")

    # Check if this is a nested path by looking for parent groups
    for parent_path, parent_info in group_fields.items():
        if not parent_path:
            continue
        if parent_path == group_path:
            continue
        # Check if group_path starts with parent_path
        if group_path.startswith(parent_path + "-"):
            parent_segments = _group_path_to_camel_segments(
                parent_path,
                parent_info["camel_name"],
                group_fields,
            )
            parent_segments.append(camel_name)
            return parent_segments

    return [camel_name]


def params_context(
    ir: DirectiveIR,
    semantics: List[Dict[str, Any]],
) -> Dict[str, Any]:
    """Build context for params template.

    Used by ``_generated/params.py.j2``.  Maps flattened RST
    options back to nested H5P JSON paths for param reconstruction.
    """
    mappings = _build_option_mappings(ir.options, semantics)

    body_mapping = None
    if ir.body is not None:
        body_path = _resolve_body_path(ir.body, semantics)
        body_mapping = {
            "field_name": ir.body.field_name,
            "field_type": ir.body.field_type,
            "param_path": body_path,
        }

    return {
        "directive_name": ir.directive_name,
        "option_mappings": [
            {
                "option_name": m.option_name,
                "field_name": m.field_name,
                "field_type": m.field_type,
                "param_path": list(m.param_path),
                "is_negated": m.is_negated,
            }
            for m in mappings
        ],
        "body_mapping": body_mapping,
    }


def _resolve_body_path(
    body: OptionIR,
    semantics: List[Dict[str, Any]],
) -> List[str]:
    """Resolve the param path for the body field."""
    group_fields = _collect_group_fields(semantics)
    path, _ = _resolve_param_path(body, group_fields)
    return path


def test_params_context(
    ir: DirectiveIR,
    semantics: List[Dict[str, Any]],
) -> Dict[str, Any]:
    """Build context for params test template.

    Used by ``tests/_generated/test_params.py.j2``.
    """
    pctx = params_context(ir, semantics)
    test_cases = []
    for m in pctx["option_mappings"]:
        test_cases.append({
            "test_name": "test_%s" % option_to_test_name(
                m["option_name"],
            ),
            "option_name": m["option_name"],
            "field_type": m["field_type"],
            "param_path": m["param_path"],
            "is_negated": m["is_negated"],
        })
    return {
        "directive_name": ir.directive_name,
        "snake_name": directive_to_snake(ir.directive_name),
        "test_cases": test_cases,
        "body_mapping": pctx["body_mapping"],
    }


def test_directive_context(
    ir: DirectiveIR,
) -> Dict[str, Any]:
    """Build context for directive test template.

    Used by ``tests/_generated/test_directive.py.j2``.
    """
    return {
        "directive_name": ir.directive_name,
        "snake_name": directive_to_snake(ir.directive_name),
        "directive_class": directive_to_directive_class(
            ir.directive_name,
        ),
        "node_class": directive_to_node_class(ir.directive_name),
        "has_body": ir.body is not None,
        "options": [
            {
                "name": opt.name,
                "field_type": opt.field_type,
                "test_name": option_to_test_name(opt.name),
            }
            for opt in ir.options
        ],
        "structural_children": [
            {
                "directive_name": c.directive_name,
                "entity": c.entity,
                "min_count": c.min_count,
            }
            for c in ir.structural_children
        ],
        "library_children": [
            {
                "directive_name": c.directive_name,
                "entity": c.entity,
            }
            for c in ir.library_children
        ],
    }


def conftest_context(
    ir: DirectiveIR,
    import_name: str,
) -> Dict[str, Any]:
    """Build context for conftest template.

    Used by ``tests/_generated/conftest.py.j2``.
    """
    return {
        "directive_name": ir.directive_name,
        "snake_name": directive_to_snake(ir.directive_name),
        "import_name": import_name,
    }


def docs_context(
    ir: DirectiveIR,
) -> Dict[str, Any]:
    """Build context for documentation template.

    Used by ``docs/_generated/directive.rst.j2``.
    """
    options_doc = []
    for opt in ir.options:
        doc_entry = {
            "name": opt.name,
            "field_type": opt.field_type,
            "label": opt.label,
            "description": opt.description,
            "required": opt.required,
            "default": opt.default,
        }
        if opt.field_type == "boolean":
            doc_entry["negated_name"] = "no-%s" % opt.name
        if opt.field_type == "select":
            doc_entry["choices"] = [
                {"value": v, "label": lbl}
                for v, lbl in opt.choices
            ]
        options_doc.append(doc_entry)

    return {
        "directive_name": ir.directive_name,
        "machine_name": ir.machine_name,
        "library": ir.library,
        "has_body": ir.body is not None,
        "body_label": ir.body.label if ir.body else None,
        "body_description": (
            ir.body.description if ir.body else None
        ),
        "options": options_doc,
        "structural_children": [
            {
                "directive_name": c.directive_name,
                "entity": c.entity,
                "min_count": c.min_count,
                "max_count": c.max_count,
                "has_body": c.body is not None,
            }
            for c in ir.structural_children
        ],
        "library_children": [
            {
                "directive_name": c.directive_name,
                "entity": c.entity,
                "libraries": list(c.libraries),
            }
            for c in ir.library_children
        ],
    }
