from __future__ import annotations

import copy
import logging
from typing import TYPE_CHECKING, Callable, List, Optional

from ...common.types import SyncAction
from ...sync_system.config import UpdateDirection
from ...sync_system.resolve_ids import IDResolver
from ...engine import decision_note, e30_update_prepare
from .compare_ops import (
    build_data_id_normalization_map,
    normalized_data_for_compare,
    collect_payload_diffs,
)

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

logger = logging.getLogger(__name__)


def _extract_snapshot_steps(snapshot) -> List:
    if not isinstance(snapshot, dict):
        return []
    steps = snapshot.get("steps")
    return copy.deepcopy(steps) if isinstance(steps, list) else []


def _steps_payload_changed(source_snapshot, target_snapshot) -> bool:
    return _extract_snapshot_steps(source_snapshot) != _extract_snapshot_steps(target_snapshot)


def _safe_repr(value, *, max_len: int = 120) -> str:
    text = repr(value)
    if len(text) <= max_len:
        return text
    return text[: max_len - 3] + "..."


def default_needs_update(
    strategy,
    source_node,
    target_node,
    resolved_data=None,
    data_id_map=None,
) -> bool:
    if resolved_data is None:
        raise ValueError(f"[{strategy.node_type}] resolved_data is required for differential sync.")

    source_data = resolved_data
    target_data = target_node.get_data()
    if source_data is None or target_data is None:
        return False

    ignore_fields = set(strategy.config.compare_ignore_fields)
    normalized_source = strategy.normalize_compare_payload(
        source_data,
        data_id_map,
        ignore_fields=ignore_fields,
    )
    normalized_target = strategy.normalize_compare_payload(
        target_data,
        data_id_map,
        ignore_fields=ignore_fields,
    )
    diff_map = strategy.get_schema_diff_validator().compare_payloads(
        normalized_source,
        normalized_target,
        sample_id=str(target_node.data_id or target_node.node_id),
        schema_refs=[strategy.schema.__name__],
    )
    is_different = bool(diff_map)

    if is_different and diff_map:
        previews = [f"{key}: {values.get('local')} != {values.get('remote')}" for key, values in list(diff_map.items())[:5]]
        logger.debug(f"[{strategy.node_type}] Node diff detected: {', '.join(previews)}")

    return is_different


def resolve_needs_update_check(strategy) -> Callable:
    return strategy._needs_update


def _collect_diff_descriptions(
    strategy,
    source_data,
    target_data,
    *,
    data_id_map,
    ignore_fields: set[str],
    max_items: int = 8,
) -> List[str]:
    if source_data is None or target_data is None:
        return []

    normalized_source = strategy.normalize_compare_payload(
        source_data,
        data_id_map,
        ignore_fields=ignore_fields,
    )
    normalized_target = strategy.normalize_compare_payload(
        target_data,
        data_id_map,
        ignore_fields=ignore_fields,
    )
    diff_map = collect_payload_diffs(
        normalized_source,
        normalized_target,
        max_items=max_items,
        ignore_fields=ignore_fields,
    )

    descriptions: List[str] = []
    for key, values in diff_map.items():
        target_value = values.get("target")
        source_value = values.get("source")
        descriptions.append(f"{key}: {_safe_repr(target_value)} -> {_safe_repr(source_value)}")
    return descriptions


async def add_update_action(
    strategy,
    *,
    from_collection: "DataCollection",
    to_collection: "DataCollection",
    needs_update_check,
    source_is_local: bool,
    data_id_map: Optional[dict] = None,
    node_filter: Optional[Callable[["SyncNode"], bool]] = None,
) -> List["SyncNode"]:
    nodes_to_update = from_collection.filter_by_state_ids(
        node_type=strategy.node_type,
        state_ids=["S01"],
    )
    if not nodes_to_update:
        return []

    if node_filter is not None:
        nodes_to_update = [n for n in nodes_to_update if node_filter(n)]
    if not nodes_to_update:
        return []

    id_resolver = IDResolver(strategy.local_collection, strategy.remote_collection, strategy.binding_manager)
    updated_nodes: List["SyncNode"] = []
    total_nodes = len(nodes_to_update)

    for idx, src_node in enumerate(nodes_to_update, start=1):
        if idx % 100 == 0 or idx == total_nodes:
            logger.debug(f"[{strategy.node_type}] 更新检查 {idx}/{total_nodes}")

        src_data = src_node.get_data()
        if not src_data:
            continue

        if source_is_local:
            target_node_id = await strategy.binding_manager.get_remote_id(strategy.node_type, src_node.node_id)
        else:
            target_node_id = await strategy.binding_manager.get_local_id(strategy.node_type, src_node.node_id)
        if not target_node_id:
            continue

        target_node = to_collection.get_by_node_id(target_node_id)
        if not target_node or target_node.action == SyncAction.CREATE:
            continue

        target_has_data_id = bool(target_node.data_id)
        data = None
        resolve_error = None
        if target_has_data_id:
            data, resolve_error = await id_resolver.resolve_and_validate(
                data=src_data,
                node_type=strategy.node_type,
                depend_fields=dict(strategy.config.depend_fields) if strategy.config.depend_fields else {},
                to_remote=source_is_local,
            )

        update_id_resolve_ok = data is not None
        has_diff = False
        diff_descriptions: List[str] = []
        source_snapshot = src_node.context.get("approval_snapshot") if isinstance(src_node.context, dict) else None
        target_snapshot = target_node.context.get("approval_snapshot") if isinstance(target_node.context, dict) else None
        steps_changed = False
        if isinstance(source_snapshot, dict):
            steps_changed = _steps_payload_changed(source_snapshot, target_snapshot)
            pending_snapshot = copy.deepcopy(source_snapshot)
            pending_snapshot["steps_changed"] = steps_changed
            target_node.context["pending_approval_snapshot"] = pending_snapshot
        else:
            target_node.context.pop("pending_approval_snapshot", None)

        if update_id_resolve_ok:
            try:
                should_update = needs_update_check(src_node, target_node, resolved_data=data, data_id_map=data_id_map)
            except TypeError:
                try:
                    should_update = needs_update_check(src_node, target_node)
                except TypeError:
                    should_update = needs_update_check(src_node)
            has_diff = bool(should_update)
            if has_diff:
                diff_descriptions = _collect_diff_descriptions(
                    strategy,
                    data,
                    target_node.get_data(),
                    data_id_map=data_id_map or {},
                    ignore_fields=set(strategy.config.compare_ignore_fields),
                )

        if steps_changed:
            has_diff = True
            diff_descriptions.append("steps: approval flow changed")

        if target_has_data_id and update_id_resolve_ok and not has_diff:
            continue

        decision = e30_update_prepare(
            strategy.ensure_runtime(),
            source_node=src_node,
            target_node=target_node,
            update_enabled=True,
            target_has_data_id=target_has_data_id,
            update_id_resolve_ok=update_id_resolve_ok,
            has_diff=has_diff,
            id_resolve_detail=resolve_error,
        )
        reason = decision_note(decision)
        if decision.to_state == "S08":
            target_node.error = reason
            logger.debug(f"[{strategy.node_type}] 更新准备阻断: target={target_node.node_id}, reason={reason}")
            continue
        if decision.to_state != "S07":
            raise RuntimeError(f"[{strategy.node_type}] unexpected update_prepare state: {decision.to_state}")
        if data is None:
            raise RuntimeError(f"[{strategy.node_type}] E30(S07) requires resolved update data")

        target_node.set_data(data)
        target_node.error = None
        diff_text = "; ".join(diff_descriptions) if diff_descriptions else "(未生成差异明细)"
        target_node.append_log(f"检测到差异: 从节点 {src_node.node_id} 更新数据 | fields={diff_text}")
        logger.info(f"[{strategy.node_type}] 更新差异字段: target={target_node.node_id}, {diff_text}")
        logger.debug(f"[{strategy.node_type}] 更新准备完成: target={target_node.node_id}, reason={reason}")
        updated_nodes.append(target_node)

    return updated_nodes


async def run_update(strategy) -> List["SyncNode"]:
    updated_nodes: List["SyncNode"] = []
    strategy.reset_schema_diff_validator()
    needs_update_check = resolve_needs_update_check(strategy)
    data_id_map = await build_data_id_normalization_map(
        node_type=strategy.node_type,
        local_collection=strategy.local_collection,
        remote_collection=strategy.remote_collection,
        binding_manager=strategy.binding_manager,
    )

    field_key = strategy.config.field_direction_key
    overrides: dict = strategy.config.field_direction_overrides

    if field_key and overrides:
        # 按方向归组：对每种出现的方向各调一次 add_update_action，传入过滤器。
        # 覆盖表中未命中的节点沿用 update_direction 作为默认方向。
        default_direction = strategy.config.update_direction
        active_directions = set(overrides.values()) | {default_direction}

        for direction in (UpdateDirection.PUSH, UpdateDirection.PULL):
            if direction not in active_directions:
                continue

            captured_direction = direction  # 避免闭包捕获循环变量
            captured_overrides = overrides
            captured_default = default_direction
            captured_field_key = field_key

            def make_filter(d=captured_direction, fk=captured_field_key, ov=captured_overrides, dflt=captured_default):
                def _filter(node: "SyncNode") -> bool:
                    node_data = node.get_data() or {}
                    key_val = node_data.get(fk)
                    effective = ov.get(key_val, dflt)
                    return effective == d
                return _filter

            node_filter = make_filter()

            if direction == UpdateDirection.PUSH:
                nodes = await add_update_action(
                    strategy,
                    from_collection=strategy.local_collection,
                    to_collection=strategy.remote_collection,
                    needs_update_check=needs_update_check,
                    source_is_local=True,
                    data_id_map=data_id_map,
                    node_filter=node_filter,
                )
                updated_nodes.extend(nodes)
                logger.info(
                    f"[{strategy.node_type}] Updated {len(nodes)} nodes in remote collection"
                    f" (Push, field_direction_key={field_key})"
                )

            elif direction == UpdateDirection.PULL:
                nodes = await add_update_action(
                    strategy,
                    from_collection=strategy.remote_collection,
                    to_collection=strategy.local_collection,
                    needs_update_check=needs_update_check,
                    source_is_local=False,
                    data_id_map=data_id_map,
                    node_filter=node_filter,
                )
                updated_nodes.extend(nodes)
                logger.info(
                    f"[{strategy.node_type}] Updated {len(nodes)} nodes in local collection"
                    f" (Pull, field_direction_key={field_key})"
                )

        return updated_nodes

    # ── 无字段级覆盖：原有逻辑不变 ──
    if strategy.config.update_direction == UpdateDirection.PUSH:
        nodes = await add_update_action(
            strategy,
            from_collection=strategy.local_collection,
            to_collection=strategy.remote_collection,
            needs_update_check=needs_update_check,
            source_is_local=True,
            data_id_map=data_id_map,
        )
        updated_nodes.extend(nodes)
        logger.info(f"[{strategy.node_type}] Updated {len(nodes)} nodes in remote collection (Push)")

    elif strategy.config.update_direction == UpdateDirection.PULL:
        nodes = await add_update_action(
            strategy,
            from_collection=strategy.remote_collection,
            to_collection=strategy.local_collection,
            needs_update_check=needs_update_check,
            source_is_local=False,
            data_id_map=data_id_map,
        )
        updated_nodes.extend(nodes)
        logger.info(f"[{strategy.node_type}] Updated {len(nodes)} nodes in local collection (Pull)")

    elif strategy.config.update_direction == UpdateDirection.BIDIRECTIONAL:
        raise NotImplementedError(
            f"[{strategy.node_type}] update_direction=BIDIRECTIONAL requires custom implementation"
        )

    return updated_nodes
