from __future__ import annotations

from enum import Enum
from typing import Dict, List, Optional

from pydantic import BaseModel, ConfigDict, Field


class OrphanAction(str, Enum):
    CREATE_REMOTE = "create_remote"
    CREATE_LOCAL = "create_local"
    DELETE_LOCAL = "delete_local"
    DELETE_REMOTE = "delete_remote"
    NONE = "none"


class UpdateDirection(str, Enum):
    PUSH = "push"
    PULL = "pull"
    BIDIRECTIONAL = "bidirectional"
    NONE = "none"


class PreBindDataIdPair(BaseModel):
    model_config = ConfigDict(validate_assignment=True, extra="forbid")

    local_data_id: str
    remote_data_id: str


class StrategyConfig(BaseModel):
    model_config = ConfigDict(validate_assignment=True, extra="forbid")

    auto_bind: bool = True
    auto_bind_fields: List[str] = Field(default_factory=list)
    pre_bind_data_id: List[PreBindDataIdPair] = Field(default_factory=list)
    depend_fields: Dict[str, str] = Field(default_factory=dict)

    local_orphan_action: OrphanAction = OrphanAction.NONE
    remote_orphan_action: OrphanAction = OrphanAction.NONE
    update_direction: UpdateDirection = UpdateDirection.PUSH

    # 字段级同步方向覆盖。
    # field_direction_key: 节点 data 中用作查表键的字段名，如 "key"。
    # field_direction_overrides: 字段值 → UpdateDirection 的映射。
    # 未命中映射的节点沿用 update_direction 作为默认方向。
    field_direction_key: Optional[str] = None
    field_direction_overrides: Dict[str, UpdateDirection] = Field(default_factory=dict)

    # post-check 一致性比较时忽略的顶层字段。
    # 只影响最终一致性评估，不影响 create/update 的差异检测。
    post_check_ignore_fields: List[str] = Field(default_factory=list)

    # schema diff / validate 时忽略的顶层字段。
    compare_ignore_fields: List[str] = Field(default_factory=list)


class StrategyRuntimeConfig(BaseModel):
    """Pipeline 层策略运行配置（按 node_type）。"""

    model_config = ConfigDict(validate_assignment=True, extra="forbid")

    node_type: str
    skip_sync: bool = False
    config: StrategyConfig = Field(default_factory=StrategyConfig)


class ConfigPresets:
    @staticmethod
    def first_sync() -> StrategyConfig:
        return StrategyConfig(
            local_orphan_action=OrphanAction.CREATE_REMOTE,
            remote_orphan_action=OrphanAction.CREATE_LOCAL,
            update_direction=UpdateDirection.PUSH,
        )

    @staticmethod
    def daily_sync() -> StrategyConfig:
        return ConfigPresets.push_only()

    @staticmethod
    def repair_sync() -> StrategyConfig:
        return StrategyConfig(
            local_orphan_action=OrphanAction.NONE,
            remote_orphan_action=OrphanAction.NONE,
            update_direction=UpdateDirection.NONE,
        )

    @staticmethod
    def pull_only() -> StrategyConfig:
        return StrategyConfig(
            local_orphan_action=OrphanAction.NONE,
            remote_orphan_action=OrphanAction.CREATE_LOCAL,
            update_direction=UpdateDirection.PULL,
        )

    @staticmethod
    def push_only() -> StrategyConfig:
        return StrategyConfig(
            local_orphan_action=OrphanAction.CREATE_REMOTE,
            remote_orphan_action=OrphanAction.NONE,
            update_direction=UpdateDirection.PUSH,
        )

    @staticmethod
    def get_preset(name: str) -> StrategyConfig:
        presets = {
            "first_sync": ConfigPresets.first_sync,
            "daily_sync": ConfigPresets.daily_sync,
            "repair_sync": ConfigPresets.repair_sync,
            "pull_only": ConfigPresets.pull_only,
            "push_only": ConfigPresets.push_only,
        }
        if name not in presets:
            raise ValueError(f"Unknown preset: {name}. Available: {list(presets.keys())}")
        return presets[name]()
