from __future__ import annotations

from collections import deque
from typing import Any

from mcp.server.fastmcp import FastMCP

from ..config import DEFAULT_PROFILE
from ..errors import QingflowApiError, raise_tool_error
from .base import ToolBase


ALLOWED_DIRECTORY_SEARCH_SCOPES = {"MEMBER", "DEPT"}


class DirectoryTools(ToolBase):
    def register(self, mcp: FastMCP) -> None:
        @mcp.tool()
        def directory_search(
            profile: str = DEFAULT_PROFILE,
            query: str = "",
            scopes: list[str] | None = None,
            page_num: int = 1,
            page_size: int = 20,
        ) -> dict[str, Any]:
            return self.directory_search(
                profile=profile,
                query=query,
                scopes=scopes,
                page_num=page_num,
                page_size=page_size,
            )

        @mcp.tool()
        def directory_list_internal_users(
            profile: str = DEFAULT_PROFILE,
            keyword: str | None = None,
            dept_id: int | None = None,
            role_id: int | None = None,
            page_num: int = 1,
            page_size: int = 20,
            contain_disable: bool = False,
        ) -> dict[str, Any]:
            return self.directory_list_internal_users(
                profile=profile,
                keyword=keyword,
                dept_id=dept_id,
                role_id=role_id,
                page_num=page_num,
                page_size=page_size,
                contain_disable=contain_disable,
            )

        @mcp.tool()
        def directory_list_all_internal_users(
            profile: str = DEFAULT_PROFILE,
            keyword: str | None = None,
            dept_id: int | None = None,
            role_id: int | None = None,
            page_size: int = 200,
            contain_disable: bool = False,
            max_pages: int = 100,
        ) -> dict[str, Any]:
            return self.directory_list_all_internal_users(
                profile=profile,
                keyword=keyword,
                dept_id=dept_id,
                role_id=role_id,
                page_size=page_size,
                contain_disable=contain_disable,
                max_pages=max_pages,
            )

        @mcp.tool()
        def directory_list_internal_departments(
            profile: str = DEFAULT_PROFILE,
            keyword: str = "",
            page_num: int = 1,
            page_size: int = 20,
        ) -> dict[str, Any]:
            return self.directory_list_internal_departments(
                profile=profile,
                keyword=keyword,
                page_num=page_num,
                page_size=page_size,
            )

        @mcp.tool()
        def directory_list_all_departments(
            profile: str = DEFAULT_PROFILE,
            parent_dept_id: int | None = None,
            max_depth: int = 20,
            max_items: int = 2000,
        ) -> dict[str, Any]:
            return self.directory_list_all_departments(
                profile=profile,
                parent_dept_id=parent_dept_id,
                max_depth=max_depth,
                max_items=max_items,
            )

        @mcp.tool()
        def directory_list_sub_departments(
            profile: str = DEFAULT_PROFILE,
            parent_dept_id: int | None = None,
        ) -> dict[str, Any]:
            return self.directory_list_sub_departments(profile=profile, parent_dept_id=parent_dept_id)

        @mcp.tool()
        def directory_list_external_members(
            profile: str = DEFAULT_PROFILE,
            keyword: str | None = None,
            page_num: int = 1,
            page_size: int = 20,
            simple: bool = False,
        ) -> dict[str, Any]:
            return self.directory_list_external_members(
                profile=profile,
                keyword=keyword,
                page_num=page_num,
                page_size=page_size,
                simple=simple,
            )

    def directory_search(
        self,
        *,
        profile: str,
        query: str,
        scopes: list[str] | None,
        page_num: int,
        page_size: int,
    ) -> dict[str, Any]:
        normalized_scopes = scopes or ["MEMBER", "DEPT"]
        invalid_scopes = [scope for scope in normalized_scopes if scope not in ALLOWED_DIRECTORY_SEARCH_SCOPES]
        if invalid_scopes:
            raise_tool_error(QingflowApiError.not_supported(f"directory_search only supports internal scopes {sorted(ALLOWED_DIRECTORY_SEARCH_SCOPES)}; got {invalid_scopes}"))
        if not query:
            raise_tool_error(QingflowApiError.config_error("query is required"))

        def runner(session_profile, context):
            result = self.backend.request(
                "POST",
                context,
                "/member/search",
                json_body={
                    "dimensions": normalized_scopes,
                    "searchKey": query,
                    "pageNum": page_num,
                    "pageSize": page_size,
                },
            )
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}

        return self._run(profile, runner)

    def directory_list_internal_users(
        self,
        *,
        profile: str,
        keyword: str | None,
        dept_id: int | None,
        role_id: int | None,
        page_num: int,
        page_size: int,
        contain_disable: bool,
    ) -> dict[str, Any]:
        def runner(session_profile, context):
            params: dict[str, Any] = {
                "pageNum": page_num,
                "pageSize": page_size,
                "containDisable": contain_disable,
            }
            if keyword:
                params["keyword"] = keyword
            if dept_id is not None:
                params["deptId"] = dept_id
            if role_id is not None:
                params["roleId"] = role_id
            result = self.backend.request("GET", context, "/contact", params=params)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}

        return self._run(profile, runner)

    def directory_list_all_internal_users(
        self,
        *,
        profile: str,
        keyword: str | None,
        dept_id: int | None,
        role_id: int | None,
        page_size: int,
        contain_disable: bool,
        max_pages: int,
    ) -> dict[str, Any]:
        if page_size <= 0:
            raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
        if max_pages <= 0:
            raise_tool_error(QingflowApiError.config_error("max_pages must be positive"))

        def runner(session_profile, context):
            current_page = 1
            fetched_pages = 0
            has_more = False
            reported_total: int | None = None
            seen_keys: set[str] = set()
            items: list[dict[str, Any]] = []
            while fetched_pages < max_pages:
                params: dict[str, Any] = {
                    "pageNum": current_page,
                    "pageSize": page_size,
                    "containDisable": contain_disable,
                }
                if keyword:
                    params["keyword"] = keyword
                if dept_id is not None:
                    params["deptId"] = dept_id
                if role_id is not None:
                    params["roleId"] = role_id
                result = self.backend.request("GET", context, "/contact", params=params)
                page_items = _directory_items(result)
                if reported_total is None:
                    reported_total = _coerce_int(_payload_value(result, "total"))
                for item in page_items:
                    if not isinstance(item, dict):
                        continue
                    member_key = _directory_member_key(item)
                    if member_key in seen_keys:
                        continue
                    seen_keys.add(member_key)
                    items.append(dict(item))
                fetched_pages += 1
                has_more = _directory_has_more(result, current_page=current_page, page_size=page_size, returned_items=len(page_items))
                if not has_more:
                    break
                current_page += 1
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "items": items,
                "pagination": {
                    "page_size": page_size,
                    "fetched_pages": fetched_pages,
                    "returned_items": len(items),
                    "reported_total": reported_total,
                    "is_complete": not has_more,
                    "has_more": has_more,
                    "next_page_num": current_page + 1 if has_more else None,
                    "max_pages": max_pages,
                },
            }

        return self._run(profile, runner)

    def directory_list_internal_departments(
        self,
        *,
        profile: str,
        keyword: str,
        page_num: int,
        page_size: int,
    ) -> dict[str, Any]:
        if not keyword:
            raise_tool_error(QingflowApiError.config_error("keyword is required"))

        def runner(session_profile, context):
            result = self.backend.request(
                "GET",
                context,
                "/contact/deptByPage",
                params={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
            )
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "page": result}

        return self._run(profile, runner)

    def directory_list_all_departments(
        self,
        *,
        profile: str,
        parent_dept_id: int | None,
        max_depth: int,
        max_items: int,
    ) -> dict[str, Any]:
        if max_depth < 0:
            raise_tool_error(QingflowApiError.config_error("max_depth must be non-negative"))
        if max_items <= 0:
            raise_tool_error(QingflowApiError.config_error("max_items must be positive"))

        def runner(session_profile, context):
            items, truncated, deepest_depth = self._walk_department_tree(
                context,
                parent_dept_id=parent_dept_id,
                max_depth=max_depth,
                max_items=max_items,
            )
            if not items and parent_dept_id is None:
                items, truncated, deepest_depth = self._walk_department_tree(
                    context,
                    parent_dept_id=0,
                    max_depth=max_depth,
                    max_items=max_items,
                )
            return {
                "profile": profile,
                "ws_id": session_profile.selected_ws_id,
                "items": items,
                "traversal": {
                    "root_parent_dept_id": parent_dept_id,
                    "returned_items": len(items),
                    "max_depth": deepest_depth,
                    "truncated": truncated,
                    "is_complete": not truncated,
                    "max_items": max_items,
                },
            }

        return self._run(profile, runner)

    def directory_list_sub_departments(self, *, profile: str, parent_dept_id: int | None) -> dict[str, Any]:
        def runner(session_profile, context):
            params: dict[str, Any] = {}
            if parent_dept_id is not None:
                params["parentDeptId"] = parent_dept_id
            result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "items": result}

        return self._run(profile, runner)

    def directory_list_external_members(
        self,
        *,
        profile: str,
        keyword: str | None,
        page_num: int,
        page_size: int,
        simple: bool,
    ) -> dict[str, Any]:
        def runner(session_profile, context):
            if simple:
                result = self.backend.request(
                    "POST",
                    context,
                    "/external/member/simple/pageList",
                    json_body={"keyword": keyword, "pageNum": page_num, "pageSize": page_size},
                )
            else:
                params: dict[str, Any] = {"pageNum": page_num, "pageSize": page_size}
                if keyword:
                    params["keyword"] = keyword
                result = self.backend.request("GET", context, "/external/member/pageList", params=params)
            return {"profile": profile, "ws_id": session_profile.selected_ws_id, "page": result, "simple": simple}

        return self._run(profile, runner)

    def _walk_department_tree(
        self,
        context,  # type: ignore[no-untyped-def]
        *,
        parent_dept_id: int | None,
        max_depth: int,
        max_items: int,
    ) -> tuple[list[dict[str, Any]], bool, int]:
        queue: deque[tuple[int | None, int]] = deque([(parent_dept_id, 0)])
        seen_ids: set[int] = set()
        requested_parents: set[int | None] = set()
        items: list[dict[str, Any]] = []
        truncated = False
        deepest_depth = 0
        while queue:
            current_parent, depth = queue.popleft()
            if current_parent in requested_parents:
                continue
            requested_parents.add(current_parent)
            params: dict[str, Any] = {}
            if current_parent is not None:
                params["parentDeptId"] = current_parent
            result = self.backend.request("GET", context, "/contact/subDeptList", params=params)
            page_items = _directory_items(result)
            for item in page_items:
                if not isinstance(item, dict):
                    continue
                dept_id = _department_id(item)
                if dept_id is None or dept_id in seen_ids:
                    continue
                seen_ids.add(dept_id)
                entry = dict(item)
                if current_parent is not None and "parentDeptId" not in entry:
                    entry["parentDeptId"] = current_parent
                entry.setdefault("depth", depth)
                items.append(entry)
                deepest_depth = max(deepest_depth, depth)
                if len(items) >= max_items:
                    truncated = True
                    return items, truncated, deepest_depth
                if depth < max_depth:
                    queue.append((dept_id, depth + 1))
                else:
                    truncated = True
        return items, truncated, deepest_depth


def _payload_value(payload: Any, key: str) -> Any:
    if isinstance(payload, dict):
        if key in payload:
            return payload.get(key)
        for container_key in ("data", "result", "page"):
            nested = payload.get(container_key)
            if isinstance(nested, dict) and key in nested:
                return nested.get(key)
    return None


def _directory_items(payload: Any) -> list[Any]:
    if isinstance(payload, list):
        return payload
    if not isinstance(payload, dict):
        return []
    for key in ("list", "items", "rows"):
        value = payload.get(key)
        if isinstance(value, list):
            return value
    for key in ("data", "result", "page"):
        nested = payload.get(key)
        if isinstance(nested, list):
            return nested
        if isinstance(nested, dict):
            for nested_key in ("list", "items", "rows", "result"):
                value = nested.get(nested_key)
                if isinstance(value, list):
                    return value
    return []


def _directory_has_more(payload: Any, *, current_page: int, page_size: int, returned_items: int) -> bool:
    page_amount = _coerce_int(_payload_value(payload, "pageAmount"))
    if page_amount is not None:
        return current_page < page_amount
    total = _coerce_int(_payload_value(payload, "total"))
    if total is not None:
        return current_page * page_size < total
    return returned_items >= page_size and returned_items > 0


def _directory_member_key(item: dict[str, Any]) -> str:
    for key in ("uid", "id", "userId"):
        value = item.get(key)
        if value is not None:
            return f"{key}:{value}"
    for key in ("email", "nickName", "name"):
        value = item.get(key)
        if isinstance(value, str) and value:
            return f"{key}:{value}"
    return repr(sorted(item.items()))


def _department_id(item: dict[str, Any]) -> int | None:
    return _coerce_int(item.get("deptId", item.get("id")))


def _coerce_int(value: Any) -> int | None:
    if isinstance(value, bool) or value is None:
        return None
    if isinstance(value, int):
        return value
    if isinstance(value, float):
        return int(value)
    if isinstance(value, str):
        text = value.strip()
        if not text:
            return None
        try:
            return int(text)
        except ValueError:
            return None
    return None
