from __future__ import annotations

import math
from datetime import UTC, datetime

from sqlalchemy import Select, func, select
from sqlalchemy.orm import Session, selectinload

from app.api.deps import CurrentUserContext
from app.models.savings import SavingsOpportunity
from app.schemas.savings import SavingsOpportunityListRead, SavingsOpportunityRead
from app.services.savings_access_policy import SavingsAccessPolicy
from app.services.savings_read_projection import SavingsReadProjection


class SavingsQueryService:
    """Savings Ledger 的租户安全查询边界。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.projection = SavingsReadProjection()

    def list_opportunities(
        self,
        current_user: CurrentUserContext,
        *,
        page: int = 1,
        page_size: int = 20,
        status: str | None = None,
        source_type: str | None = None,
        value_kind: str | None = None,
        department_id: str | None = None,
        project_code: str | None = None,
        expense_type: str | None = None,
        supplier_id: str | None = None,
        city: str | None = None,
        owner_id: str | None = None,
        claim_id: str | None = None,
        created_from: datetime | None = None,
        created_to: datetime | None = None,
        sort: str = "created_desc",
    ) -> SavingsOpportunityListRead:
        statement = select(SavingsOpportunity).where(
            SavingsOpportunity.tenant_id == self._tenant(current_user)
        )
        statement = self._apply_user_scope(statement, current_user)
        statement = self._apply_filters(
            statement,
            status=status,
            source_type=source_type,
            value_kind=value_kind,
            department_id=department_id,
            project_code=project_code,
            expense_type=expense_type,
            supplier_id=supplier_id,
            city=city,
            owner_id=owner_id,
            claim_id=claim_id,
            created_from=created_from,
            created_to=created_to,
        )

        count_statement = select(func.count()).select_from(statement.order_by(None).subquery())
        total = int(self.db.scalar(count_statement) or 0)
        statement = (
            self._apply_sort(statement, sort).offset((page - 1) * page_size).limit(page_size)
        )
        opportunities = list(self.db.scalars(statement).all())
        items = [
            self.projection.opportunity_read(item, current_user, include_details=False)
            for item in opportunities
        ]
        return SavingsOpportunityListRead(
            items=items,
            total=total,
            page=page,
            page_size=page_size,
            total_pages=math.ceil(total / page_size) if total else 0,
            generated_at=datetime.now(UTC),
        )

    def get_opportunity(
        self,
        opportunity_id: str,
        current_user: CurrentUserContext,
    ) -> SavingsOpportunityRead | None:
        statement = (
            select(SavingsOpportunity)
            .options(
                selectinload(SavingsOpportunity.baseline_snapshot),
                selectinload(SavingsOpportunity.realizations),
                selectinload(SavingsOpportunity.evidence_links),
                selectinload(SavingsOpportunity.events),
            )
            .where(
                SavingsOpportunity.tenant_id == self._tenant(current_user),
                SavingsOpportunity.id == opportunity_id,
            )
        )
        opportunity = self.db.scalar(statement)
        if opportunity is None:
            return None
        permission = SavingsAccessPolicy.opportunity_permission(opportunity, current_user)
        if not permission.can_read:
            return None
        return self.projection.opportunity_read(opportunity, current_user)

    @classmethod
    def _apply_user_scope(
        cls,
        statement: Select[tuple[SavingsOpportunity]],
        current_user: CurrentUserContext,
    ) -> Select[tuple[SavingsOpportunity]]:
        return SavingsAccessPolicy.apply_opportunity_read_scope(
            statement,
            current_user,
            include_owner=True,
        )

    @staticmethod
    def _apply_filters(
        statement: Select[tuple[SavingsOpportunity]],
        *,
        status: str | None,
        source_type: str | None,
        value_kind: str | None,
        department_id: str | None,
        project_code: str | None,
        expense_type: str | None,
        supplier_id: str | None,
        city: str | None,
        owner_id: str | None,
        claim_id: str | None,
        created_from: datetime | None,
        created_to: datetime | None,
    ) -> Select[tuple[SavingsOpportunity]]:
        scalar_filters = (
            (SavingsOpportunity.status, status),
            (SavingsOpportunity.source_type, source_type),
            (SavingsOpportunity.value_kind, value_kind),
            (SavingsOpportunity.owner_id, owner_id),
            (SavingsOpportunity.claim_id, claim_id),
        )
        for column, value in scalar_filters:
            normalized = str(value or "").strip()
            if normalized:
                statement = statement.where(column == normalized)

        dimension_filters = (
            ("department_id", department_id),
            ("project_code", project_code),
            ("expense_type", expense_type),
            ("supplier_id", supplier_id),
            ("city", city),
        )
        for key, value in dimension_filters:
            normalized = str(value or "").strip()
            if normalized:
                statement = statement.where(
                    SavingsOpportunity.dimension_json[key].as_string() == normalized
                )
        if created_from is not None:
            statement = statement.where(SavingsOpportunity.created_at >= created_from)
        if created_to is not None:
            statement = statement.where(SavingsOpportunity.created_at <= created_to)
        return statement

    @staticmethod
    def _apply_sort(
        statement: Select[tuple[SavingsOpportunity]],
        sort: str,
    ) -> Select[tuple[SavingsOpportunity]]:
        sort_map = {
            "created_asc": (SavingsOpportunity.created_at.asc(), SavingsOpportunity.id.asc()),
            "due_asc": (
                SavingsOpportunity.due_at.asc().nullslast(),
                SavingsOpportunity.created_at.desc(),
            ),
            "estimated_desc": (
                SavingsOpportunity.estimated_net.desc(),
                SavingsOpportunity.created_at.desc(),
            ),
            "created_desc": (
                SavingsOpportunity.created_at.desc(),
                SavingsOpportunity.id.desc(),
            ),
        }
        return statement.order_by(*sort_map.get(sort, sort_map["created_desc"]))

    @staticmethod
    def _tenant(current_user: CurrentUserContext) -> str:
        return str(current_user.tenant_id or "default").strip() or "default"
