from __future__ import annotations

from dataclasses import dataclass

from sqlalchemy import Select, or_

from app.api.deps import CurrentUserContext
from app.models.savings import SavingsOpportunity, SavingsRealization
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy

SAVINGS_READ_ROLE_CODES = frozenset({"finance", "executive"})
SAVINGS_MANAGE_ROLE_CODES = frozenset({"finance", "executive"})
SAVINGS_SCOPED_READ_ROLE_CODES = frozenset({"budget_monitor"})


class SavingsPermissionError(PermissionError):
    pass


@dataclass(frozen=True, slots=True)
class SavingsPermission:
    can_read: bool
    available_actions: tuple[str, ...] = ()
    read_only_reason: str = ""


class SavingsAccessPolicy:
    """统一 Savings 列表、详情和动作的服务端业务权限。"""

    @staticmethod
    def role_codes(current_user: CurrentUserContext) -> set[str]:
        return ExpenseClaimAccessPolicy.normalize_role_codes(current_user)

    @classmethod
    def can_read_tenant_value(cls, current_user: CurrentUserContext) -> bool:
        return bool(current_user.is_admin or cls.role_codes(current_user) & SAVINGS_READ_ROLE_CODES)

    @classmethod
    def can_read_scoped_value(cls, current_user: CurrentUserContext) -> bool:
        return bool(
            cls.role_codes(current_user) & SAVINGS_SCOPED_READ_ROLE_CODES
            and cls._scope_values(current_user)
        )

    @classmethod
    def can_manage_savings(cls, current_user: CurrentUserContext) -> bool:
        # 平台管理员只有在同时持有财务业务角色时才可改变节省事实。
        return bool(cls.role_codes(current_user) & SAVINGS_MANAGE_ROLE_CODES)

    @classmethod
    def require_tenant_value_read(cls, current_user: CurrentUserContext) -> None:
        if not (
            cls.can_read_tenant_value(current_user)
            or cls.can_read_scoped_value(current_user)
        ):
            raise SavingsPermissionError("当前用户无权访问企业经营价值数据。")

    @classmethod
    def apply_opportunity_read_scope(
        cls,
        statement: Select,
        current_user: CurrentUserContext,
        *,
        include_owner: bool,
    ) -> Select:
        if cls.can_read_tenant_value(current_user):
            return statement
        conditions = []
        if cls.can_read_scoped_value(current_user):
            for key, value in cls._scope_values(current_user):
                conditions.append(
                    SavingsOpportunity.dimension_json[key].as_string() == value
                )
        if include_owner:
            conditions.extend(
                SavingsOpportunity.owner_id.ilike(actor_id)
                for actor_id in cls._actor_ids(current_user)
            )
        return statement.where(or_(*conditions)) if conditions else statement.where(False)

    @classmethod
    def opportunity_permission(
        cls,
        opportunity: SavingsOpportunity,
        current_user: CurrentUserContext,
    ) -> SavingsPermission:
        if str(opportunity.tenant_id) != str(current_user.tenant_id):
            return SavingsPermission(False, (), "该节省机会不在当前租户范围内。")

        is_owner = cls._matches_actor(opportunity.owner_id, current_user)
        can_read = (
            cls.can_read_tenant_value(current_user)
            or cls._matches_data_scope(opportunity, current_user)
            or is_owner
        )
        if not can_read:
            return SavingsPermission(False, (), "当前用户不是该节省机会的负责人。")

        can_manage = cls.can_manage_savings(current_user) or is_owner
        actions: list[str] = []
        status = str(opportunity.status or "").strip()
        if can_manage:
            if status == "identified":
                actions.extend(("accept", "reject"))
            elif status == "accepted":
                actions.extend(("start", "reject"))
            elif status == "in_progress":
                actions.append("record_realization")
        if cls.can_manage_savings(current_user) and status in {
            "identified",
            "accepted",
            "in_progress",
        }:
            actions.append("expire")

        if actions:
            return SavingsPermission(True, tuple(dict.fromkeys(actions)), "")
        if current_user.is_admin and not cls.can_manage_savings(current_user):
            reason = "管理员可查看经营价值，但未持有财务业务角色，不能改变节省事实。"
        elif status in {"realized", "verified", "reversed", "rejected", "expired"}:
            reason = "当前机会状态仅允许查看审计记录。"
        else:
            reason = "当前用户没有可执行的节省机会动作。"
        return SavingsPermission(True, (), reason)

    @classmethod
    def realization_permission(
        cls,
        realization: SavingsRealization,
        opportunity: SavingsOpportunity,
        current_user: CurrentUserContext,
    ) -> SavingsPermission:
        opportunity_permission = cls.opportunity_permission(opportunity, current_user)
        if not opportunity_permission.can_read:
            return opportunity_permission

        if not cls.can_manage_savings(current_user):
            return SavingsPermission(
                True,
                (),
                "只有持有财务或管理层业务角色的人员可以确认节省结果。",
            )

        actor_ids = cls._actor_ids(current_user)
        excluded_ids = {
            str(opportunity.owner_id or "").strip().casefold(),
            str(realization.recorded_by_id or "").strip().casefold(),
        }
        excluded_ids.discard("")
        status = str(realization.status or "").strip()

        if status == "pending_confirmation":
            if actor_ids & excluded_ids:
                return SavingsPermission(
                    True,
                    (),
                    "机会负责人或结果填报人不能确认自己的节省结果。",
                )
            return SavingsPermission(True, ("confirm", "reject"), "")
        if (
            status == "finance_confirmed"
            and realization.realization_type == "actual"
            and realization.reversed_at is None
        ):
            return SavingsPermission(True, ("reverse",), "")
        if realization.realization_type == "reversal":
            return SavingsPermission(True, (), "冲回是只追加的财务事实，不能再次冲回。")
        if realization.reversed_at is not None:
            return SavingsPermission(True, (), "该确认结果已经通过负向事实完成冲回。")
        return SavingsPermission(True, (), "该实际结果已经结束，当前仅可查看审计记录。")

    @classmethod
    def require_opportunity_action(
        cls,
        opportunity: SavingsOpportunity,
        current_user: CurrentUserContext,
        action: str,
    ) -> None:
        permission = cls.opportunity_permission(opportunity, current_user)
        if action not in permission.available_actions:
            raise SavingsPermissionError(permission.read_only_reason or "当前用户无权执行该动作。")

    @classmethod
    def require_realization_action(
        cls,
        realization: SavingsRealization,
        opportunity: SavingsOpportunity,
        current_user: CurrentUserContext,
        action: str,
    ) -> None:
        permission = cls.realization_permission(realization, opportunity, current_user)
        if action not in permission.available_actions:
            raise SavingsPermissionError(permission.read_only_reason or "当前用户无权执行该动作。")

    @classmethod
    def _matches_actor(cls, owner_id: str | None, current_user: CurrentUserContext) -> bool:
        normalized_owner = str(owner_id or "").strip().casefold()
        return bool(normalized_owner and normalized_owner in cls._actor_ids(current_user))

    @classmethod
    def _matches_data_scope(
        cls,
        opportunity: SavingsOpportunity,
        current_user: CurrentUserContext,
    ) -> bool:
        if not cls.can_read_scoped_value(current_user):
            return False
        dimensions = dict(opportunity.dimension_json or {})
        return any(
            str(dimensions.get(key) or "").strip() == value
            for key, value in cls._scope_values(current_user)
        )

    @staticmethod
    def _scope_values(current_user: CurrentUserContext) -> tuple[tuple[str, str], ...]:
        candidates = (
            ("department_id", str(current_user.department_id or "").strip()),
            ("department_name", str(current_user.department_name or "").strip()),
            ("cost_center", str(current_user.cost_center or "").strip()),
        )
        return tuple((key, value) for key, value in candidates if value)

    @staticmethod
    def _actor_ids(current_user: CurrentUserContext) -> set[str]:
        return {
            value.casefold()
            for value in (
                str(current_user.employee_id or "").strip(),
                str(current_user.username or "").strip(),
                str(current_user.employee_no or "").strip(),
            )
            if value
        }
