from __future__ import annotations

from typing import Any

from fastapi import HTTPException, status
from sqlalchemy import and_, func, not_, or_, select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.agent_run import AgentRun
from app.schemas.agent_run import AgentRunRead
from app.services.finance_dashboard_access_policy import FinanceDashboardAccessPolicy
from app.services.finance_dashboard_scope import (
    FINANCE_DASHBOARD_TASK_TYPE,
    resolve_finance_dashboard_data_scope,
)


class AgentRunAccessPolicy:
    """对租户边界和敏感领域权限做返回前的第二层校验。"""

    @classmethod
    def build_query_scope(cls, current_user: CurrentUserContext) -> Any:
        """把财务快照领域门禁下推到 limit 之前，避免不可见记录挤占窗口。"""
        tenant_id = cls.require_current_tenant_id(current_user)
        route_task_type = func.coalesce(
            AgentRun.route_json["task_type"].as_string(),
            "",
        )
        route_job_type = func.coalesce(
            AgentRun.route_json["job_type"].as_string(),
            "",
        )
        is_finance_snapshot = or_(
            route_task_type == FINANCE_DASHBOARD_TASK_TYPE,
            route_job_type == FINANCE_DASHBOARD_TASK_TYPE,
        )
        if not FinanceDashboardAccessPolicy.can_read(current_user):
            return not_(is_finance_snapshot)

        expected_data_scope = resolve_finance_dashboard_data_scope(tenant_id)
        valid_finance_scope = and_(
            AgentRun.route_json["tenant_id"].as_string() == tenant_id,
            AgentRun.ontology_json["tenant_id"].as_string() == tenant_id,
            AgentRun.route_json["data_scope"].as_string() == expected_data_scope,
            AgentRun.ontology_json["data_scope"].as_string() == expected_data_scope,
        )
        return or_(not_(is_finance_snapshot), valid_finance_scope)

    @classmethod
    def filter_list_items(
        cls,
        runs: list[AgentRunRead],
        current_user: CurrentUserContext,
        db: Session,
    ) -> list[AgentRunRead]:
        payloads_by_run_id = cls._run_scope_payloads(
            db,
            [run.run_id for run in runs],
        )
        current_tenant_id = cls.require_current_tenant_id(current_user)
        visible: list[AgentRunRead] = []
        for run in runs:
            payloads = payloads_by_run_id.get(run.run_id)
            if payloads is None:
                continue
            route_json, ontology_json = payloads
            if cls._tenant_scope_from_payloads(route_json, ontology_json) != current_tenant_id:
                continue
            if cls.is_finance_dashboard_snapshot(run) and not cls._can_read_finance_snapshot_scope(
                cls._finance_scope_from_payloads(route_json, ontology_json),
                current_user,
            ):
                continue
            visible.append(run)
        return visible

    @classmethod
    def _run_scope_payloads(
        cls,
        db: Session,
        run_ids: list[str],
    ) -> dict[str, tuple[object, object]]:
        if not run_ids:
            return {}
        rows = db.execute(
            select(AgentRun.run_id, AgentRun.route_json, AgentRun.ontology_json).where(
                AgentRun.run_id.in_(run_ids)
            )
        ).all()
        return {
            str(run_id): (route_json, ontology_json) for run_id, route_json, ontology_json in rows
        }

    @classmethod
    def require_detail_read(
        cls,
        run: AgentRunRead,
        current_user: CurrentUserContext,
    ) -> None:
        current_tenant_id = cls.require_current_tenant_id(current_user)
        if cls._tenant_scope_from_payloads(run.route_json, run.ontology_json) != current_tenant_id:
            cls._raise_not_found()
        if not cls.is_finance_dashboard_snapshot(run):
            return

        run_scope = cls._finance_scope_from_payloads(run.route_json, run.ontology_json)
        expected_scope = (
            resolve_finance_dashboard_data_scope(current_tenant_id) if current_tenant_id else None
        )
        if (
            current_tenant_id is None
            or run_scope is None
            or run_scope != (current_tenant_id, expected_scope)
        ):
            cls._raise_not_found()
        FinanceDashboardAccessPolicy.require_read(current_user)

    @classmethod
    def is_finance_dashboard_snapshot(cls, run: AgentRunRead) -> bool:
        route = run.route_json if isinstance(run.route_json, dict) else {}
        return any(
            str(route.get(key) or "").strip() == FINANCE_DASHBOARD_TASK_TYPE
            for key in ("task_type", "job_type")
        )

    @classmethod
    def _can_read_finance_snapshot_scope(
        cls,
        run_scope: tuple[str, str] | None,
        current_user: CurrentUserContext,
    ) -> bool:
        current_tenant_id = cls._normalized_tenant_id(current_user.tenant_id)
        expected_scope = (
            resolve_finance_dashboard_data_scope(current_tenant_id)
            if current_tenant_id is not None
            else None
        )
        return bool(
            current_tenant_id
            and run_scope
            and run_scope == (current_tenant_id, expected_scope)
            and FinanceDashboardAccessPolicy.can_read(current_user)
        )

    @classmethod
    def _finance_scope_from_payloads(
        cls,
        *payloads: object,
    ) -> tuple[str, str] | None:
        tenant_ids: list[str] = []
        data_scopes: list[str] = []
        for payload in payloads:
            if not isinstance(payload, dict):
                return None
            tenant_id = cls._normalized_tenant_id(payload.get("tenant_id"))
            data_scope = str(payload.get("data_scope") or "").strip()
            if tenant_id is None or not data_scope:
                return None
            tenant_ids.append(tenant_id)
            data_scopes.append(data_scope)
        if not tenant_ids or len(set(tenant_ids)) != 1 or len(set(data_scopes)) != 1:
            return None
        return tenant_ids[0], data_scopes[0]

    @classmethod
    def _tenant_scope_from_payloads(
        cls,
        *payloads: object,
    ) -> str | None:
        tenant_ids: list[str] = []
        for payload in payloads:
            if not isinstance(payload, dict) or "tenant_id" not in payload:
                return None
            tenant_id = cls._normalized_tenant_id(payload.get("tenant_id"))
            if tenant_id is None:
                return None
            tenant_ids.append(tenant_id)
        if len(tenant_ids) != len(payloads) or len(set(tenant_ids)) != 1:
            return None
        return tenant_ids[0]

    @classmethod
    def require_current_tenant_id(cls, current_user: CurrentUserContext) -> str:
        tenant_id = cls._normalized_tenant_id(current_user.tenant_id)
        if tenant_id is None:
            cls._raise_not_found()
        return tenant_id

    @staticmethod
    def _raise_not_found() -> None:
        # 统一按不存在处理，避免 run_id 或作用域标记成为租户探针。
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Run not found",
        )

    @staticmethod
    def _normalized_tenant_id(value: object) -> str | None:
        normalized = str(value or "").strip()
        return normalized or None
