FlattenedField = tuple[str, str, bool]  # name, type, is_root


def _split_top_level(value: str) -> list[str]:
    """
    Split a type definition into top-level entries.

    We only split on commas that are not nested inside angle brackets
    (e.g. struct<...>, array<...>) or parentheses (e.g. decimal(10,2)).

    Example:
        input = "p:decimal(10,2),v:varchar(255),c:char(3)"
        output = ["p:decimal(10,2)", "v:varchar(255)", "c:char(3)"]
    """
    parts: list[str] = []
    angle_bracket_depth = 0  # nesting level inside '<' and '>'
    parenthesis_depth = 0  # nesting level inside '(' and ')'
    start = 0
    for idx, ch in enumerate(value):
        if ch == "<":
            angle_bracket_depth += 1
        elif ch == ">":
            angle_bracket_depth -= 1
        elif ch == "(":
            parenthesis_depth += 1
        elif ch == ")":
            parenthesis_depth -= 1
        # Only split at commas that are not nested.
        elif ch == "," and angle_bracket_depth == 0 and parenthesis_depth == 0:
            parts.append(value[start:idx].strip())
            start = idx + 1
    tail = value[start:].strip()
    if tail:
        parts.append(tail)
    return parts


def _base_primitive(type_str: str) -> str:
    """
    Normalize parameterized primitives to their base name.

    Example:
        "decimal(10,2)" -> "decimal"
        "varchar(255)" -> "varchar"
        "string" -> "string"
    """
    lowered = type_str.strip().lower()
    if "(" in lowered:
        base = lowered.split("(", 1)[0].strip()
        if base:
            return base
    return lowered


def _flatten_struct_fields(inner: str, prefix: str) -> list[FlattenedField]:
    """
    Flatten struct fields without emitting the struct container.

    Example:
        inner = "a:int,b:struct<c:string>"
        prefix = "root"
        output = [
            ("root.a", "int", False),
            ("root.b", "struct", False),
            ("root.b.c", "string", False),
        ]
    """
    flattened: list[FlattenedField] = []
    for field in _split_top_level(inner):
        name, nested_type = field.split(":", 1)
        name = name.strip()
        nested_type = nested_type.strip()
        child_prefix = f"{prefix}.{name}" if prefix else name
        flattened.extend(
            _flatten_type(nested_type, child_prefix, is_root=False)
        )
    return flattened


def _flatten_struct(
    inner: str, prefix: str, *, is_root: bool
) -> list[FlattenedField]:
    """Flatten a struct body into dotted paths."""
    return [(prefix, "struct", is_root), *_flatten_struct_fields(inner, prefix)]


def _flatten_array(
    inner: str, prefix: str, *, is_root: bool
) -> list[FlattenedField]:
    """Flatten array types while honoring array rules."""
    flattened: list[FlattenedField] = [(prefix, "array", is_root)]
    if inner.startswith("struct<") and inner.endswith(">"):
        inner_struct = inner[len("struct<") : -1].strip()
        flattened.extend(_flatten_struct_fields(inner_struct, prefix))
    return flattened


def _flatten_type(
    type_str: str, prefix: str, *, is_root: bool
) -> list[FlattenedField]:
    """
    Flatten Glue/Hive types into dotted paths following catalog rules.

    Rules:
    - struct -> emit "<col>, struct" then each field as "<col>.<field>, <type>"
    - array<struct<...>> -> emit "<col>, array" then recurse into struct fields
      under "<col>.<field>"
    - array<primitive/map/array/...> -> emit "<col>, array" and stop
    - map<*,*> -> emit "<col>, map" and stop
    - primitive (incl. decimal(10,2), varchar(n), char(n)) -> emit base primitive
    """
    type_str = type_str.strip()
    if type_str.startswith("struct<") and type_str.endswith(">"):
        inner = type_str[len("struct<") : -1].strip()
        return _flatten_struct(inner, prefix, is_root=is_root)

    if type_str.startswith("array<") and type_str.endswith(">"):
        inner = type_str[len("array<") : -1].strip()
        return _flatten_array(inner, prefix, is_root=is_root)

    if type_str.startswith("map<") and type_str.endswith(">"):
        return [(prefix, "map", is_root)]

    # Primitive type: emit the field with its base primitive (e.g., decimal/varchar/char).
    return [(prefix, _base_primitive(type_str), is_root)]


def flatten_glue_type(
    column_name: str,
    column_type: str,
) -> list[FlattenedField]:
    """
    Expand a column type into container and leaf paths.

    This normalizes Glue/Hive types into dotted field paths suitable for
    schema/metadata catalogs.

    Examples:
        flatten_glue_type("tags", "array<string>")
            -> [("tags", "array", True)]

        flatten_glue_type("metrics", "struct<p:decimal(10,2),v:varchar(255)>")
            -> [
                ("metrics", "struct", True),
                ("metrics.p", "decimal", False),
                ("metrics.v", "varchar", False),
            ]
    """
    return _flatten_type(column_type, column_name, is_root=True)
