"""
Project Detail API Handler

职责：
- 加载项目容量信息（project_detail）
- 更新项目容量信息（对应项目详情更新接口）

实现的接口：
PUT:
- /project/plan_construction - 更新计划施工
- /project/actual_construction - 更新实际施工
- /project/ensure_capacity - 更新确保产能
- /project/climb_capacity - 更新登高产能
- /project/challenge_capacity - 更新揭榜产能
- /project/production_capacity - 更新投产数据

不支持: CREATE, DELETE
"""

import asyncio
from typing import List, Dict, Any, Optional, Tuple, Set
from pydantic import ValidationError

from ...datasource.api.handler import BaseApiHandler
from ...datasource.task_result import TaskResult
from ...common.sync_node import SyncNode
from ...datasource.api.errors import ERROR_VALIDATION_FAILED
from .sync_node import ProjectDetailSyncNode
from schemas.project_extensions.project_detail import ProjectDetailProject, ProjectDetailKey
from schemas.project.project_base import (
    ProjectPlanConstruction,
    ProjectActualConstruction,
    ProjectEnsureCapacity,
    ProjectClimbCapacity,
    ProjectChallengeCapacity,
    ProjectProductionCapacity,
)


_PRODUCTION_PROJECT_FIELDS = [
    "plan_production_date",
    "plan_full_production_date",
    "ic_plan_first_production_date",
    "ic_plan_full_production_date",
    "actual_production_date",
    "actual_full_production_date",
]


class ProjectDetailApiHandler(BaseApiHandler):
    """项目容量信息 API Handler"""

    def __init__(self):
        super().__init__()
        self._node_type = "project_detail_data"
        self._node_class = ProjectDetailSyncNode
        self._schema = ProjectDetailProject
        self._pending_push_context: Dict[str, Dict[str, str]] = {}

    async def load(self) -> List[SyncNode]:
        """加载项目容量信息（从 project all_info 中提取）"""
        from ..project.api_handler import ProjectApiHandler

        projects = self._collection.filter(node_type="project")
        if not projects:
            print("⚠️ No projects found, skipping project_detail load")
            return []

        nodes: List[SyncNode] = []
        for project in projects:
            project_id = project.data_id

            try:
                all_info = project.context.get("all_info")
                if not all_info:
                    all_info = await ProjectApiHandler.get_all_info(self.api_client, project_id)

                project_detail_list = all_info.get("project_detail", [])

                print(f"  Project {project_id}: {len(project_detail_list)} project_detail records")

                for detail_data in project_detail_list:
                    try:
                        candidate_data = dict(detail_data) if isinstance(detail_data, dict) else detail_data
                        if isinstance(candidate_data, dict) and not candidate_data.get("project_id"):
                            candidate_data["project_id"] = project_id

                        validated = self._schema.model_validate(candidate_data)
                        detail_dict = validated.model_dump()

                        node = self._create_node(detail_dict)
                        node.context["project_id"] = project_id
                        nodes.append(node)
                    except Exception as e:
                        print(f"⚠️ Skipping invalid project_detail: {e}")
                        continue

            except Exception as e:
                print(f"❌ Failed to load project_detail for project {project_id}: {e}")
                continue

        return nodes

    async def create_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
        """创建语义通过 project 详情更新接口实现（服务端隐式创建）。"""
        return await self._upsert_all(nodes, force_execute=True)

    async def update_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
        """批量更新项目容量信息"""
        return await self._upsert_all(nodes, force_execute=False)

    async def _upsert_all(self, nodes: List[SyncNode], *, force_execute: bool) -> List[TaskResult]:
        """批量 upsert：对 project_detail_data 统一走 project update 类接口。"""
        results: List[TaskResult] = []
        seen_project_key: Set[Tuple[str, str]] = set()

        for node in nodes:
            node_data = node.get_data()
            if node_data is None:
                error_msg = node.error or "Node data is missing or invalid"
                results.append(TaskResult.failed(node_id=node.node_id, error=f"Cannot upsert: {error_msg}"))
                continue

            origin_data = node.get_origin_data() or {}
            key = self._normalize_key(node_data.get("key"))
            project_id = node_data.get("project_id") or origin_data.get("project_id")

            if not project_id:
                results.append(TaskResult.failed(node_id=node.node_id, error="Missing project_id for upsert"))
                continue
            if not key:
                results.append(TaskResult.failed(node_id=node.node_id, error="Missing key for upsert"))
                continue

            pair = (str(project_id), str(key))
            if pair in seen_project_key:
                results.append(
                    TaskResult.failed(
                        node_id=node.node_id,
                        error=f"Duplicate project_detail_data in one batch: project_id={project_id}, key={key}",
                    )
                )
                continue
            seen_project_key.add(pair)

            project_data: Dict[str, Any] = {}
            project_origin_data: Dict[str, Any] = {}
            project_node = self._collection.get_by_data_id("project", project_id)
            if project_node:
                project_data = project_node.get_data() or {}
                project_origin_data = project_node.get_origin_data() or {}
            if key == "production":
                if project_data:
                    self._merge_production_fields(node_data, project_data)
                if project_origin_data:
                    self._merge_production_fields(origin_data, project_origin_data)

            config = self._get_update_config(key)
            if not config:
                results.append(TaskResult.failed(node_id=node.node_id, error=f"Unsupported project_detail key: {key}"))
                continue

            schema, api_func = config

            new_payload = self._build_update_payload(key, node_data)
            origin_payload = self._build_update_payload(key, origin_data)

            if new_payload is None:
                results.append(TaskResult.failed(node_id=node.node_id, error=f"Unable to build payload for key={key}"))
                continue
            if origin_payload is None:
                origin_payload = {}

            force_steps_update = self._steps_changed(node)
            steps_payload = self._get_request_steps(node)
            if not force_execute and not force_steps_update and not self._payload_changed(schema, new_payload, origin_payload):
                reason = f"No physical diff found for project_detail {key} despite strategy update request (Normalization Discrepancy)."
                print(f"  [WARNING] {reason}")
                # 记录为跳过
                results.append(TaskResult.skipped(node_id=node.node_id, reason=reason))
                continue

            try:
                schema.model_validate(new_payload)
            except ValidationError as e:
                results.append(TaskResult.failed(node_id=node.node_id, error=f"{ERROR_VALIDATION_FAILED}: {e}"))
                continue

            push_id = self._generate_push_id()
            biz_id = project_id
            self._pending_push_context[push_id] = {
                "project_id": str(project_id),
                "key": str(key),
            }

            try:
                await api_func(self.api_client, new_payload, push_id, biz_id, steps=steps_payload)
                results.append(
                    TaskResult.in_progress(
                        node_id=node.node_id,
                        task_id=push_id,
                        push_id=push_id,
                        project_id=str(project_id),
                        key=str(key),
                    )
                )
                self._mark_approval_snapshot_synced(node)
            except Exception as e:
                self._pending_push_context.pop(push_id, None)
                results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))

        return results

    async def delete_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
        """不支持删除"""
        return [
            TaskResult.failed(node_id=node.node_id, error="Project detail deletion not supported")
            for node in nodes
        ]

    async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
        """轮询异步任务状态"""
        polled = await super().poll_tasks(task_ids)
        results: Dict[str, TaskResult] = {}
        success_context_by_task: Dict[str, Dict[str, str]] = {}
        project_order: List[str] = []
        task_ids_by_project: Dict[str, List[str]] = {}

        for task_id in task_ids:
            result = polled.get(task_id)
            if result is None:
                continue

            if result.status == result.status.IN_PROGRESS:
                results[task_id] = result
                continue

            context = self._pending_push_context.get(task_id)
            if context is None:
                results[task_id] = result
                continue

            if result.status == result.status.FAILED:
                self._pending_push_context.pop(task_id, None)
                results[task_id] = result
                continue

            project_id = context["project_id"]

            if result.data_id and result.data_id != project_id:
                self._pending_push_context.pop(task_id, None)
                results[task_id] = result
                continue

            success_context_by_task[task_id] = context
            if project_id not in task_ids_by_project:
                project_order.append(project_id)
                task_ids_by_project[project_id] = []
            task_ids_by_project[project_id].append(task_id)

        for project_id in project_order:
            task_ids_for_project = task_ids_by_project.get(project_id, [])
            try:
                all_info = await self._get_project_all_info(project_id, force_refresh=True)
            except Exception as exc:
                for task_id in task_ids_for_project:
                    self._pending_push_context.pop(task_id, None)
                    results[task_id] = TaskResult.failed(error=f"Poll resolve data_id failed: {exc}", push_id=task_id)
                continue

            for task_id in task_ids_for_project:
                context = success_context_by_task[task_id]
                key = context["key"]

                try:
                    resolved_data_id = self._resolve_data_id_from_all_info(all_info, project_id=project_id, key=key)
                    if not resolved_data_id:
                        resolved_data_id = await self._resolve_data_id_with_retry(project_id=project_id, key=key)
                except Exception as exc:
                    self._pending_push_context.pop(task_id, None)
                    results[task_id] = TaskResult.failed(error=f"Poll resolve data_id failed: {exc}", push_id=task_id)
                    continue

                if not resolved_data_id:
                    self._pending_push_context.pop(task_id, None)
                    results[task_id] = TaskResult.failed(
                        error=(
                            "Poll resolve data_id failed: "
                            f"project_detail not found in all_info for project_id={project_id}, key={key}"
                        ),
                        push_id=task_id,
                    )
                    continue

                self._pending_push_context.pop(task_id, None)
                results[task_id] = TaskResult.success(data_id=resolved_data_id, push_id=task_id)

        return results

    def _create_node(self, data: Dict[str, Any]) -> SyncNode:
        project_id_raw = data.get("project_id")
        key_raw = self._normalize_key(data.get("key"))
        project_id = str(project_id_raw) if project_id_raw else ""
        key = str(key_raw) if key_raw else ""
        loaded_data_id = str(data.get("id") or data.get("_id") or "")

        existing_by_pair = self._find_existing_by_project_key(project_id=project_id, key=key)
        if existing_by_pair is not None:
            if loaded_data_id and existing_by_pair.data_id != loaded_data_id:
                with existing_by_pair.allow_core_state_write("project_detail_pair_rebind"):
                    existing_by_pair.data_id = loaded_data_id
            existing_by_pair.set_data(data)
            existing_by_pair.set_origin_data(data)
            if project_id:
                existing_by_pair.context["project_id"] = project_id
            return existing_by_pair

        node = self._create_basic_node(data, depend_ids=[])
        if project_id:
            node.context["project_id"] = project_id
        return node

    def _find_existing_by_project_key(self, *, project_id: str, key: str) -> Optional[SyncNode]:
        if not project_id or not key or self._collection is None:
            return None

        candidates = self._collection.filter(
            node_type=self.node_type,
            node_filter=lambda n: (
                ((n.get_data() or {}).get("project_id") == project_id)
                and (self._normalize_key((n.get_data() or {}).get("key")) == key)
            ),
        )
        if not candidates:
            return None
        if len(candidates) > 1:
            raise RuntimeError(
                f"Duplicate project_detail_data detected: project_id={project_id}, key={key}, count={len(candidates)}"
            )
        return candidates[0]

    async def _get_project_all_info(self, project_id: str, *, force_refresh: bool = False) -> Dict[str, Any]:
        from ..project.api_handler import ProjectApiHandler

        project_node = self._collection.get_by_data_id("project", project_id)
        if project_node is not None and not force_refresh:
            all_info = project_node.context.get("all_info")
            if isinstance(all_info, dict):
                return all_info

        if force_refresh:
            ProjectApiHandler._all_info_cache.pop(project_id, None)

        all_info = await ProjectApiHandler.get_all_info(self.api_client, project_id)
        if project_node is not None:
            project_node.context["all_info"] = all_info
        return all_info

    def _resolve_data_id_from_all_info(self, all_info: Dict[str, Any], *, project_id: str, key: str) -> Optional[str]:
        details = all_info.get("project_detail", [])
        if not isinstance(details, list):
            return None

        matched: List[str] = []
        for item in details:
            if not isinstance(item, dict):
                continue
            item_key = self._normalize_key(item.get("key"))
            if item_key != key:
                continue
            item_id = item.get("id") or item.get("_id")
            if not item_id:
                raise ValueError(f"project_detail item missing id for project_id={project_id}, key={key}")
            matched.append(str(item_id))

        if len(matched) > 1:
            raise ValueError(f"project_detail duplicated for project_id={project_id}, key={key}")
        if len(matched) == 1:
            return matched[0]
        return None

    async def _resolve_data_id_with_retry(self, *, project_id: str, key: str) -> Optional[str]:
        attempts = 6
        delay = 0.2

        for attempt in range(attempts):
            all_info = await self._get_project_all_info(project_id, force_refresh=True)
            resolved = self._resolve_data_id_from_all_info(all_info, project_id=project_id, key=key)
            if resolved:
                return resolved
            if attempt < attempts - 1:
                await asyncio.sleep(delay)

        return None

    async def _resolve_data_id_from_project_all_info(self, project_id: str, key: str) -> Optional[str]:
        all_info = await self._get_project_all_info(project_id)
        return self._resolve_data_id_from_all_info(all_info, project_id=project_id, key=key)

    def _normalize_key(self, key: Any) -> Optional[str]:
        if isinstance(key, ProjectDetailKey):
            return key.value
        if isinstance(key, str):
            return key
        return None

    def _get_update_config(self, key: Optional[str]) -> Optional[Tuple[type, Any]]:
        if not key:
            return None
        return {
            "plan_construction": (ProjectPlanConstruction, api_update_project_plan_construction),
            "actual_construction": (ProjectActualConstruction, api_update_project_actual_construction),
            "ensure_production": (ProjectEnsureCapacity, api_update_project_ensure_capacity),
            "climb_production": (ProjectClimbCapacity, api_update_project_climb_capacity),
            "challenge_production": (ProjectChallengeCapacity, api_update_project_challenge_capacity),
            "production": (ProjectProductionCapacity, api_update_project_production_capacity),
        }.get(key)

    def _build_update_payload(self, key: Optional[str], data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not key:
            return None

        value_list = data.get("value") or []
        if key == "plan_construction":
            return {
                "plan_construction_list": self._build_capacity_list(value_list, require_date=True)
            }
        if key == "actual_construction":
            return {
                "actual_construction_list": self._build_capacity_list(value_list, require_date=True)
            }
        if key == "ensure_production":
            return {
                "ensure_production_capacity_list": self._build_capacity_list(value_list, require_year=True)
            }
        if key == "climb_production":
            return {
                "climb_production_capacity_list": self._build_capacity_list(value_list, require_year=True)
            }
        if key == "challenge_production":
            return {
                "challenge_production_capacity_list": self._build_capacity_list(value_list, require_year=True)
            }
        if key == "production":
            payload: Dict[str, Any] = {
                "actual_production_list": self._build_capacity_list(value_list, require_date=True)
            }
            payload.update(self._extract_production_fields(data))
            return payload

        return None

    def _build_capacity_list(
        self,
        value_list: List[Dict[str, Any]],
        require_year: bool = False,
        require_date: bool = False,
    ) -> List[Dict[str, Any]]:
        items: List[Dict[str, Any]] = []
        for item in value_list:
            if not isinstance(item, dict):
                continue

            year = item.get("year")
            date = item.get("date")
            capacity = item.get("capacity")

            if require_year and (year is None or year == ""):
                continue
            if require_date and (date is None or date == ""):
                continue

            payload_item: Dict[str, Any] = {"capacity": capacity}
            if require_year:
                payload_item["year"] = year
            if require_date:
                payload_item["date"] = date

            items.append(payload_item)

        return items

    def _payload_changed(self, schema: type, new_payload: Dict[str, Any], origin_payload: Dict[str, Any]) -> bool:
        try:
            new_dump = schema.model_validate(new_payload).model_dump()
        except ValidationError:
            return True

        try:
            origin_dump = schema.model_validate(origin_payload).model_dump()
        except ValidationError:
            return True

        return new_dump != origin_dump

    def _extract_production_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
        return {k: data.get(k) for k in _PRODUCTION_PROJECT_FIELDS if k in data}

    def _merge_production_fields(self, target: Dict[str, Any], project_data: Dict[str, Any]) -> None:
        for field in _PRODUCTION_PROJECT_FIELDS:
            if (field not in target or target.get(field) in (None, "")) and field in project_data:
                target[field] = project_data.get(field)


# ========== 静态 API 函数（可抽成 SDK） ==========

async def api_update_project_plan_construction(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
    """PUT /project/plan_construction - 更新计划施工"""
    ProjectPlanConstruction.model_validate(data)
    request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
    response = await client.request(method="PUT", endpoint="/project/plan_construction", data=request_data)
    if response.get("code") != 200:
        raise Exception(f"API error: {response.get('message', 'Unknown error')}")


async def api_update_project_actual_construction(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
    """PUT /project/actual_construction - 更新实际施工"""
    ProjectActualConstruction.model_validate(data)
    request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
    response = await client.request(method="PUT", endpoint="/project/actual_construction", data=request_data)
    if response.get("code") != 200:
        raise Exception(f"API error: {response.get('message', 'Unknown error')}")


async def api_update_project_ensure_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
    """PUT /project/ensure_capacity - 更新确保产能"""
    ProjectEnsureCapacity.model_validate(data)
    request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
    response = await client.request(method="PUT", endpoint="/project/ensure_capacity", data=request_data)
    if response.get("code") != 200:
        raise Exception(f"API error: {response.get('message', 'Unknown error')}")


async def api_update_project_climb_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
    """PUT /project/climb_capacity - 更新登高产能"""
    ProjectClimbCapacity.model_validate(data)
    request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
    response = await client.request(method="PUT", endpoint="/project/climb_capacity", data=request_data)
    if response.get("code") != 200:
        raise Exception(f"API error: {response.get('message', 'Unknown error')}")


async def api_update_project_challenge_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
    """PUT /project/challenge_capacity - 更新揭榜产能"""
    ProjectChallengeCapacity.model_validate(data)
    request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
    response = await client.request(method="PUT", endpoint="/project/challenge_capacity", data=request_data)
    if response.get("code") != 200:
        raise Exception(f"API error: {response.get('message', 'Unknown error')}")


async def api_update_project_production_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
    """PUT /project/production_capacity - 更新投产数据"""
    ProjectProductionCapacity.model_validate(data)
    request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
    response = await client.request(method="PUT", endpoint="/project/production_capacity", data=request_data)
    if response.get("code") != 200:
        raise Exception(f"API error: {response.get('message', 'Unknown error')}")
