from __future__ import annotations

from datetime import UTC, datetime

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.approval_task import ApprovalTask
from app.models.financial_record import ExpenseClaim
from app.services.approval_task_projection import ApprovalTaskProjectionService
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin

OPEN_TASK_STATUSES = ("waiting", "pending")


class ApprovalTaskProjectionRefreshService:
    """在风险事实变化后，同事务刷新开放任务的只读队列投影。"""

    def __init__(self, db: Session) -> None:
        self.db = db
        self.projections = ApprovalTaskProjectionService(db)

    def refresh_claim(
        self,
        *,
        tenant_id: str,
        claim_id: str,
        now: datetime | None = None,
    ) -> list[str]:
        tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(tenant_id)
        normalized_claim_id = str(claim_id or "").strip()
        if not normalized_claim_id:
            return []
        claim_statement = select(ExpenseClaim).where(
            ExpenseClaim.id == normalized_claim_id,
            ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(tenant),
        )
        task_statement = (
            select(ApprovalTask)
            .where(
                ApprovalTask.tenant_id == tenant,
                ApprovalTask.claim_id == normalized_claim_id,
                ApprovalTask.status.in_(OPEN_TASK_STATUSES),
            )
            .order_by(ApprovalTask.id.asc())
        )
        bind = self.db.get_bind()
        if bind is not None and bind.dialect.name == "postgresql":
            claim_statement = claim_statement.with_for_update()
            task_statement = task_statement.with_for_update()
        claim = self.db.scalar(claim_statement.execution_options(populate_existing=True))
        if claim is None:
            return []

        generated_at = _as_utc(now or datetime.now(UTC))
        tasks = list(
            self.db.scalars(
                task_statement.execution_options(populate_existing=True)
            ).all()
        )
        for task in tasks:
            projection = self.projections.build(
                claim,
                tenant_id=tenant,
                entered_at=_as_utc(task.entered_at),
                now=generated_at,
                sla_hours=task.sla_hours_snapshot,
            )
            escalation_reasons = [
                dict(reason)
                for reason in list(task.priority_reasons_json or [])
                if str(reason.get("code") or "").startswith("sla_escalated_l")
            ]
            reasons = [
                dict(reason)
                for reason in projection.priority_reasons_json
                if not (
                    str(reason.get("code") or "") == "routine" and escalation_reasons
                )
            ]
            reasons.extend(escalation_reasons)
            task.priority_reasons_json = _unique_reasons(reasons)
            task.priority_score = min(
                100,
                sum(int(reason.get("weight") or 0) for reason in task.priority_reasons_json),
            )
            task.priority_tier = (
                "urgent"
                if task.priority_score >= 65
                else "high"
                if task.priority_score >= 40
                else "normal"
            )
            task.risk_level = projection.risk_level
            task.open_risk_count = projection.open_risk_count
            task.evidence_completeness = projection.evidence_completeness
            task.batch_eligible = projection.batch_eligible
            task.batch_block_reasons_json = list(projection.batch_block_reasons_json)
            task.projection_updated_at = generated_at
            task.updated_at = generated_at
        if tasks:
            self.db.flush()
        return [task.id for task in tasks]


def _unique_reasons(reasons: list[dict]) -> list[dict]:
    unique: dict[str, dict] = {}
    for index, reason in enumerate(reasons):
        code = str(reason.get("code") or "reason").strip()
        unique[f"{code}:{index}" if code in unique else code] = reason
    return list(unique.values())


def _as_utc(value: datetime) -> datetime:
    if value.tzinfo is None:
        return value.replace(tzinfo=UTC)
    return value.astimezone(UTC)
