from blends.models import (
    NId,
)
from blends.stack.attributes import (
    STACK_GRAPH_IS_REFERENCE,
)
from blends.stack.edges import (
    Edge,
    add_edge,
)
from blends.stack.node_helpers import (
    pop_scoped_symbol_node_attributes,
    root_node_attributes,
)
from blends.stack.policies.registry import (
    get_export_policy,
)
from blends.syntax.models import (
    SyntaxGraphArgs,
)
from blends.utilities.text_nodes import (
    node_to_str,
)

_PATTERN_NODE_TYPES: frozenset[str] = frozenset(
    {"pattern_list", "tuple_pattern", "list_pattern", "list_splat_pattern"}
)


def extract_pattern_identifier_symbols(
    args: SyntaxGraphArgs, *, pattern_id: NId
) -> list[tuple[NId, str]]:
    label_type = args.ast_graph.nodes.get(pattern_id, {}).get("label_type")
    if label_type == "identifier":
        symbol = node_to_str(args.ast_graph, pattern_id)
        return [(pattern_id, symbol)] if symbol else []
    if label_type in _PATTERN_NODE_TYPES:
        return [
            result
            for child_id in args.ast_graph.successors(pattern_id)
            for result in extract_pattern_identifier_symbols(args, pattern_id=child_id)
        ]
    return []


def add_pattern_binding_nodes(args: SyntaxGraphArgs, *, var_id: NId) -> None:
    scope_stack = args.metadata.setdefault("scope_stack", [])
    parent_scope = scope_stack[-1] if scope_stack else None
    for identifier_nid, symbol in extract_pattern_identifier_symbols(args, pattern_id=var_id):
        synthetic_nid = get_next_synthetic_node_id(args)
        precedence = get_next_binding_precedence(args)
        args.syntax_graph.add_node(
            synthetic_nid,
            label_type="SyntheticPatternBinding",
            **pop_scoped_symbol_node_attributes(symbol=symbol, precedence=precedence),
        )
        if parent_scope:
            add_edge(
                args.syntax_graph,
                Edge(source=parent_scope, sink=synthetic_nid, precedence=precedence),
            )
        if args.syntax_graph.nodes.get(identifier_nid):
            args.syntax_graph.update_node(identifier_nid, {STACK_GRAPH_IS_REFERENCE: False})


def bound_identifier_symbol(args: SyntaxGraphArgs, *, var_id: NId) -> str | None:
    var_node = args.ast_graph.nodes.get(var_id, {})
    if var_node.get("label_type") != "identifier":
        return None
    symbol = node_to_str(args.ast_graph, var_id)
    return symbol if symbol else None


def get_next_binding_precedence(args: SyntaxGraphArgs) -> int:
    scope_stack = args.metadata.get("scope_stack", [])
    if not scope_stack:
        return 0
    return 1


def is_definition_exported(args: SyntaxGraphArgs) -> bool:
    policy = get_export_policy(args.language)
    scope_stack = args.metadata.get("scope_stack", [])
    parent_scope = scope_stack[-1] if scope_stack else None
    return policy.is_definition_exported(args.syntax_graph, args.n_id, parent_scope)


def get_next_synthetic_node_id(args: SyntaxGraphArgs) -> NId:
    current_id = args.metadata.get("next_synthetic_id")
    if current_id is None:
        msg = "next_synthetic_id not initialized in metadata"
        raise ValueError(msg)
    args.metadata["next_synthetic_id"] = current_id + 1
    return NId(current_id)


def get_or_create_root_nid(args: SyntaxGraphArgs) -> NId:
    existing = args.metadata.get("root_nid")
    if existing is not None:
        return existing
    root_nid = get_next_synthetic_node_id(args)
    args.syntax_graph.add_node(root_nid, **root_node_attributes())
    args.metadata["root_nid"] = root_nid
    return root_nid
