from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple

from ...common.types import BindingStatus
from ...sync_system.config import OrphanAction
from ...sync_system.resolve_ids import IDResolver
from ...sync_system.utils import extract_biz_key_dict, dict_to_biz_key_tuple, resolve_id_fields_in_key_dict
from ...engine import e10_bind_core, decision_note, e15_bind_dependency, e16_bind_auto

if TYPE_CHECKING:
    from ...common.collection import DataCollection
    from ...common.sync_node import SyncNode

logger = logging.getLogger(__name__)


async def run_bind(strategy, **kwargs):
    logger.info(f"[{strategy.node_type}] Starting Binding Pipeline...")

    local_nodes: List["SyncNode"] = strategy.local_collection.filter_by_state_ids(
        node_type=strategy.node_type,
        state_ids=["S00"],
    )
    remote_nodes: List["SyncNode"] = strategy.remote_collection.filter_by_state_ids(
        node_type=strategy.node_type,
        state_ids=["S00"],
    )

    await phase_core_state(strategy, local_nodes, remote_nodes)
    await phase_dependency_check(strategy, local_nodes, remote_nodes)
    await phase_auto_binding(strategy, local_nodes, remote_nodes)
    await refresh_bind_data_id(strategy)

    logger.info(f"[{strategy.node_type}] Binding Pipeline Completed.")


async def refresh_bind_data_id(strategy) -> None:
    binding_pairs = await strategy.binding_manager.get_all_bindings(strategy.node_type)
    local_to_remote = {local_id: remote_id for local_id, remote_id in binding_pairs if local_id}
    remote_to_local = {remote_id: local_id for local_id, remote_id in binding_pairs if local_id and remote_id}

    for local_node in strategy.local_collection.filter(node_type=strategy.node_type):
        remote_node_id = local_to_remote.get(local_node.node_id)
        remote_node = strategy.remote_collection.get_by_node_id(remote_node_id) if remote_node_id else None
        if remote_node and remote_node.data_id:
            local_node.context["bind_data_id"] = remote_node.data_id
        else:
            local_node.context.pop("bind_data_id", None)

    for remote_node in strategy.remote_collection.filter(node_type=strategy.node_type):
        local_node_id = remote_to_local.get(remote_node.node_id)
        local_node = strategy.local_collection.get_by_node_id(local_node_id) if local_node_id else None
        if local_node and local_node.data_id:
            remote_node.context["bind_data_id"] = local_node.data_id
        else:
            remote_node.context.pop("bind_data_id", None)


async def phase_core_state(strategy, local_nodes: List["SyncNode"], remote_nodes: List["SyncNode"]):
    remote_map = {n.node_id: n for n in remote_nodes}
    processed_local_ids: Set[str] = set()
    processed_remote_ids: Set[str] = set()

    for node in local_nodes:
        remote_id = await strategy.binding_manager.get_remote_id(strategy.node_type, node.node_id)

        if remote_id:
            remote_node = remote_map.get(remote_id)

            local_data = node.get_data()
            remote_data = remote_node.get_data() if remote_node else None
            local_valid = local_data is not None
            peer_valid = remote_data is not None
            local_decision = e10_bind_core(
                strategy.ensure_runtime(),
                node=node,
                has_binding_record=True,
                local_data=local_data,
                peer_data=remote_data,
                local_valid=local_valid,
                peer_valid=peer_valid,
            )
            local_reason = decision_note(local_decision)
            if local_decision.to_state in {"S03", "S04"}:
                logger.debug(f"[{strategy.node_type}] 核心绑定阻断: node={node.node_id}, reason={local_reason}")
            else:
                logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={node.node_id}, reason={local_reason}")
            processed_local_ids.add(node.node_id)

            if remote_node:
                peer_decision = e10_bind_core(
                    strategy.ensure_runtime(),
                    node=remote_node,
                    has_binding_record=True,
                    local_data=remote_data,
                    peer_data=local_data,
                    local_valid=peer_valid,
                    peer_valid=local_valid,
                )
                peer_reason = decision_note(peer_decision)
                if peer_decision.to_state in {"S03", "S04"}:
                    logger.debug(
                        f"[{strategy.node_type}] 核心绑定阻断: node={remote_node.node_id}, reason={peer_reason}"
                    )
                else:
                    logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={remote_node.node_id}, reason={peer_reason}")
                processed_remote_ids.add(remote_node.node_id)

    for node in local_nodes:
        if node.node_id not in processed_local_ids:
            decision = e10_bind_core(
                strategy.ensure_runtime(),
                node=node,
                has_binding_record=False,
            )
            reason = decision_note(decision)
            logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={node.node_id}, reason={reason}")

    for r_node in remote_nodes:
        if r_node.node_id not in processed_remote_ids:
            decision = e10_bind_core(
                strategy.ensure_runtime(),
                node=r_node,
                has_binding_record=False,
            )
            reason = decision_note(decision)
            logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={r_node.node_id}, reason={reason}")


def check_dependency_satisfied(
    dependency_nodes: List[Optional["SyncNode"]],
    field_values: Optional[List[str]] = None,
    *,
    allow_empty_dependency_ref: bool = True,
    allow_missing_dep_data_id: bool = True,
) -> bool:
    if not dependency_nodes:
        return True

    values = field_values or []
    for index, dep_node in enumerate(dependency_nodes):
        dep_value = values[index] if index < len(values) else ""
        dep_value_str = "" if dep_value is None else str(dep_value)

        if dep_node is None:
            if allow_empty_dependency_ref and dep_value_str == "":
                continue
            return False
        if dep_node.binding_status != BindingStatus.NORMAL:
            return False
        if not dep_node.data_id and not allow_missing_dep_data_id:
            return False
    return True


def collect_dependency_errors(
    dependency_nodes: List[Optional["SyncNode"]],
    field_names: List[str],
    field_types: List[str],
    field_values: List[str],
    *,
    allow_empty_dependency_ref: bool = True,
    allow_missing_dep_data_id: bool = True,
) -> List[str]:
    errors: List[str] = []
    for index, dep_node in enumerate(dependency_nodes):
        field_name = field_names[index]
        field_type = field_types[index] if index < len(field_types) else "unknown"
        field_value = field_values[index] if index < len(field_values) else ""
        field_value_str = "" if field_value is None else str(field_value)

        if dep_node is None:
            if allow_empty_dependency_ref and field_value_str == "":
                continue
            errors.append(f"not_found|{field_name}|{field_type}|{field_value_str}")
            continue

        if dep_node.binding_status != BindingStatus.NORMAL:
            errors.append(f"bad_status|{field_name}|{dep_node.binding_status.value}")
            continue

        if not dep_node.data_id and not allow_missing_dep_data_id:
            status_info = f"status={dep_node.status.value}" if dep_node.status else "status=未知"
            errors.append(f"no_data_id|{field_name}|{status_info}")

    return errors


async def phase_dependency_check(strategy, local_nodes: List["SyncNode"], remote_nodes: List["SyncNode"]):
    await check_dependencies_for_nodes(strategy, local_nodes, strategy.local_collection)
    await check_dependencies_for_nodes(strategy, remote_nodes, strategy.remote_collection)


async def check_dependencies_for_nodes(strategy, nodes: List["SyncNode"], collection: "DataCollection"):
    for node in nodes:
        if node.binding_status != BindingStatus.MISSING:
            continue

        has_depend_fields = bool(strategy.config.depend_fields)
        node_data = node.get_data()
        data_present = bool(node_data)

        depend_ids = []
        depend_nodes = []
        depend_field_names: List[str] = []
        depend_field_types: List[str] = []
        depend_field_values: List[str] = []
        semantic_errors: List[str] = []

        if has_depend_fields and data_present:
            for field_name, dep_node_type in strategy.config.depend_fields.items():
                dep_data_id = node_data.get(field_name)
                if not dep_data_id:
                    continue

                dep_node = collection.get_by_data_id(dep_node_type, dep_data_id)
                if dep_node is None:
                    maybe_node_id_ref = collection.get_by_node_id(str(dep_data_id))
                    if maybe_node_id_ref is not None and maybe_node_id_ref.node_type == dep_node_type:
                        semantic_errors.append(f"依赖字段 {field_name} 使用了 node_id={dep_data_id}，期望 data_id 引用")

                depend_nodes.append(dep_node)
                depend_field_names.append(field_name)
                depend_field_types.append(dep_node_type)
                depend_field_values.append(str(dep_data_id))

                if dep_node:
                    depend_ids.append(dep_node.node_id)

        node.depend_ids = depend_ids

        allow_empty_dependency_ref = True
        allow_missing_dep_data_id = True

        dep_satisfied = (
            check_dependency_satisfied(
                depend_nodes,
                depend_field_values,
                allow_empty_dependency_ref=allow_empty_dependency_ref,
                allow_missing_dep_data_id=allow_missing_dep_data_id,
            )
            if has_depend_fields and data_present
            else True
        )
        dependency_errors = (
            collect_dependency_errors(
                depend_nodes,
                depend_field_names,
                depend_field_types,
                depend_field_values,
                allow_empty_dependency_ref=allow_empty_dependency_ref,
                allow_missing_dep_data_id=allow_missing_dep_data_id,
            )
            if has_depend_fields and data_present
            else []
        )

        decision = e15_bind_dependency(
            strategy.ensure_runtime(),
            node=node,
            data_present=data_present,
            has_depend_fields=has_depend_fields,
            dep_satisfied=dep_satisfied,
            dep_errors=dependency_errors,
            semantic_errors=semantic_errors,
        )
        reason = decision_note(decision)
        if decision.to_state in {"S03", "S04", "S05"}:
            detail_reason = node.error or reason
            logger.debug(f"[{strategy.node_type}] 依赖检查阻断: node={node.node_id}, reason={detail_reason}")


async def phase_auto_binding(strategy, local_nodes: List["SyncNode"], remote_nodes: List["SyncNode"]):
    pending_auto_bind_updates: List[Dict[str, Any]] = []
    prebound_node_ids: Set[str] = set()

    local_s02_ids = {
        n.node_id
        for n in strategy.local_collection.filter_by_state_ids(
            node_type=strategy.node_type,
            state_ids=["S02"],
        )
    }
    remote_s02_ids = {
        n.node_id
        for n in strategy.remote_collection.filter_by_state_ids(
            node_type=strategy.node_type,
            state_ids=["S02"],
        )
    }

    local_create_enabled = strategy.config.local_orphan_action == OrphanAction.CREATE_REMOTE
    remote_create_enabled = strategy.config.remote_orphan_action == OrphanAction.CREATE_LOCAL

    for prebind_pair in strategy.config.pre_bind_data_id:
        local_node = strategy.local_collection.get_by_data_id(strategy.node_type, prebind_pair.local_data_id)
        remote_node = strategy.remote_collection.get_by_data_id(strategy.node_type, prebind_pair.remote_data_id)

        if local_node is None or remote_node is None:
            continue
        if local_node.node_id not in local_s02_ids or remote_node.node_id not in remote_s02_ids:
            continue
        if local_node.binding_status != BindingStatus.MISSING or remote_node.binding_status != BindingStatus.MISSING:
            continue

        existing_remote = await strategy.binding_manager.get_remote_id(strategy.node_type, local_node.node_id)
        existing_local = await strategy.binding_manager.get_local_id(strategy.node_type, remote_node.node_id)
        if existing_remote or existing_local:
            continue

        pending_auto_bind_updates.append(
            {
                "node": local_node,
                "outcome": "ONE_TO_ONE",
                "create_enabled": None,
                "bind_remote_node_id": remote_node.node_id,
            }
        )
        pending_auto_bind_updates.append(
            {
                "node": remote_node,
                "outcome": "ONE_TO_ONE",
                "create_enabled": None,
                "bind_remote_node_id": None,
            }
        )
        prebound_node_ids.add(local_node.node_id)
        prebound_node_ids.add(remote_node.node_id)

    if not strategy.config.auto_bind or not strategy.config.auto_bind_fields:
        for node in local_nodes:
            if node.node_id in prebound_node_ids:
                continue
            if node.binding_status == BindingStatus.MISSING:
                pending_auto_bind_updates.append({"node": node, "outcome": None, "create_enabled": None})
        for node in remote_nodes:
            if node.node_id in prebound_node_ids:
                continue
            if node.binding_status == BindingStatus.MISSING:
                pending_auto_bind_updates.append({"node": node, "outcome": None, "create_enabled": None})
    else:
        id_resolver = IDResolver(strategy.local_collection, strategy.remote_collection, strategy.binding_manager)

        local_key_map: Dict[Tuple, List["SyncNode"]] = {}
        remote_key_map: Dict[Tuple, List["SyncNode"]] = {}

        for node in local_nodes:
            if node.node_id in prebound_node_ids:
                continue
            if node.node_id not in local_s02_ids:
                continue
            node_data = node.get_data()
            if not node_data:
                if node.binding_status == BindingStatus.MISSING:
                    pending_auto_bind_updates.append(
                        {
                            "node": node,
                            "outcome": "DATA_MISSING",
                            "create_enabled": None,
                            "bind_remote_node_id": None,
                        }
                    )
                continue

            missing_fields = [fn for fn in strategy.config.auto_bind_fields if fn not in node_data]
            if missing_fields:
                if node.binding_status == BindingStatus.MISSING:
                    pending_auto_bind_updates.append(
                        {"node": node, "outcome": "KEY_MISSING", "create_enabled": None, "bind_remote_node_id": None}
                    )
                continue

            key_dict = extract_biz_key_dict(node_data, strategy.config.auto_bind_fields)
            if not key_dict:
                continue

            resolved_key_dict, resolve_error = await resolve_id_fields_in_key_dict(
                key_dict,
                id_resolver,
                strategy._get_id_field_hints(),
            )
            if not resolved_key_dict:
                if node.binding_status == BindingStatus.MISSING:
                    pending_auto_bind_updates.append(
                        {
                            "node": node,
                            "outcome": "KEY_ID_RESOLVE_FAIL",
                            "create_enabled": None,
                            "bind_remote_node_id": None,
                            "detail": resolve_error,
                        }
                    )
                continue

            key = dict_to_biz_key_tuple(resolved_key_dict, strategy.config.auto_bind_fields)
            local_key_map.setdefault(key, []).append(node)

        for r_node in remote_nodes:
            if r_node.node_id in prebound_node_ids:
                continue
            if r_node.node_id not in remote_s02_ids:
                continue
            r_node_data = r_node.get_data()
            if not r_node_data:
                if r_node.binding_status == BindingStatus.MISSING:
                    pending_auto_bind_updates.append(
                        {
                            "node": r_node,
                            "outcome": "DATA_MISSING",
                            "create_enabled": None,
                            "bind_remote_node_id": None,
                        }
                    )
                continue

            missing_fields = [fn for fn in strategy.config.auto_bind_fields if fn not in r_node_data]
            if missing_fields:
                if r_node.binding_status == BindingStatus.MISSING:
                    pending_auto_bind_updates.append(
                        {
                            "node": r_node,
                            "outcome": "KEY_MISSING",
                            "create_enabled": None,
                            "bind_remote_node_id": None,
                        }
                    )
                continue

            key_dict = extract_biz_key_dict(r_node_data, strategy.config.auto_bind_fields)
            if key_dict:
                key = dict_to_biz_key_tuple(key_dict, strategy.config.auto_bind_fields)
                remote_key_map.setdefault(key, []).append(r_node)

        all_keys = set(local_key_map.keys()) | set(remote_key_map.keys())

        for biz_key in all_keys:
            local_candidates = local_key_map.get(biz_key, [])
            remote_candidates = remote_key_map.get(biz_key, [])

            excluded_local = set()
            excluded_remote = set()

            for l_node in local_candidates:
                if l_node.binding_status != BindingStatus.NORMAL:
                    continue

                remote_id = await strategy.binding_manager.get_remote_id(strategy.node_type, l_node.node_id)
                if not remote_id:
                    continue

                for r_node in remote_candidates:
                    if r_node.node_id == remote_id and r_node.binding_status == BindingStatus.NORMAL:
                        excluded_local.add(l_node.node_id)
                        excluded_remote.add(r_node.node_id)
                        break

            l_remain = [n for n in local_candidates if n.node_id not in excluded_local]
            r_remain = [n for n in remote_candidates if n.node_id not in excluded_remote]

            if len(l_remain) == 1 and len(r_remain) == 1:
                l_node = l_remain[0]
                r_node = r_remain[0]

                if l_node.binding_status == BindingStatus.MISSING and r_node.binding_status == BindingStatus.MISSING:
                    pending_auto_bind_updates.append(
                        {
                            "node": l_node,
                            "outcome": "ONE_TO_ONE",
                            "create_enabled": None,
                            "bind_remote_node_id": r_node.node_id,
                        }
                    )
                    pending_auto_bind_updates.append(
                        {
                            "node": r_node,
                            "outcome": "ONE_TO_ONE",
                            "create_enabled": None,
                            "bind_remote_node_id": None,
                        }
                    )
            else:
                if len(l_remain) >= 1 and len(r_remain) == 0:
                    for n in l_remain:
                        if n.binding_status == BindingStatus.MISSING:
                            pending_auto_bind_updates.append(
                                {
                                    "node": n,
                                    "outcome": "ZERO",
                                    "create_enabled": local_create_enabled,
                                    "bind_remote_node_id": None,
                                }
                            )

                if len(r_remain) >= 1 and len(l_remain) == 0:
                    for n in r_remain:
                        if n.binding_status == BindingStatus.MISSING:
                            pending_auto_bind_updates.append(
                                {
                                    "node": n,
                                    "outcome": "ZERO",
                                    "create_enabled": remote_create_enabled,
                                    "bind_remote_node_id": None,
                                }
                            )

                if (len(l_remain) > 1 and len(r_remain) >= 1) or (len(l_remain) == 1 and len(r_remain) > 1):
                    for n in l_remain:
                        if n.binding_status == BindingStatus.MISSING:
                            pending_auto_bind_updates.append(
                                {
                                    "node": n,
                                    "outcome": "AMBIGUOUS",
                                    "create_enabled": None,
                                    "bind_remote_node_id": None,
                                }
                            )

                if (len(r_remain) > 1 and len(l_remain) >= 1) or (len(r_remain) == 1 and len(l_remain) > 1):
                    for n in r_remain:
                        if n.binding_status == BindingStatus.MISSING:
                            pending_auto_bind_updates.append(
                                {
                                    "node": n,
                                    "outcome": "AMBIGUOUS",
                                    "create_enabled": None,
                                    "bind_remote_node_id": None,
                                }
                            )

    for item in pending_auto_bind_updates:
        node = item["node"]
        auto_bind_outcome = item["outcome"]
        create_enabled = item["create_enabled"]
        bind_remote_node_id = item.get("bind_remote_node_id")
        detail = item.get("detail")
        auto_bind_enabled = auto_bind_outcome is not None
        decision = e16_bind_auto(
            strategy.ensure_runtime(),
            node=node,
            auto_bind_enabled=auto_bind_enabled,
            auto_bind_outcome=auto_bind_outcome,
            create_enabled=create_enabled,
            id_resolve_detail=detail,
        )
        reason = decision_note(decision)
        if decision.to_state == "S01" and auto_bind_outcome == "ONE_TO_ONE":
            if bind_remote_node_id:
                await strategy.binding_manager.bind(
                    strategy.node_type,
                    node.node_id,
                    bind_remote_node_id,
                )
            logger.debug(f"[{strategy.node_type}] 自动绑定成功: node={node.node_id}")
        if decision.to_state in {"S04", "S05"}:
            if detail:
                reason = f"{reason} | detail={detail}"
            node.error = reason
            logger.debug(f"[{strategy.node_type}] 自动绑定阻断: node={node.node_id}, reason={reason}")
