from __future__ import annotations

import hashlib
from collections.abc import Callable

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.db.session import get_session_factory
from app.models.approval_action import ApprovalActionLedger
from app.models.approval_task import ApprovalTask
from app.schemas.approval_task import (
    ApprovalTaskBatchApproveCreate,
    ApprovalTaskBatchApproveRead,
    ApprovalTaskBatchItemRead,
)
from app.schemas.reimbursement import ExpenseClaimRead
from app.services.approval_action_protocol import ApprovalActionConflictError
from app.services.approval_task_access_policy import ApprovalTaskPermissionError
from app.services.approval_task_protocol import ApprovalTaskConflictError
from app.services.expense_claim_risk_gate import ExpenseClaimRiskBlockedError
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
from app.services.expense_claims import ExpenseClaimService


class ApprovalTaskBatchService:
    """小批量审批编排；每一项使用独立数据库事务。"""

    def __init__(
        self,
        session_factory: Callable[[], Session] | None = None,
    ) -> None:
        self.session_factory = session_factory or get_session_factory()

    def approve(
        self,
        payload: ApprovalTaskBatchApproveCreate,
        current_user: CurrentUserContext,
    ) -> ApprovalTaskBatchApproveRead:
        results = [
            self._approve_one(
                item=item,
                batch_request_id=payload.batch_request_id,
                current_user=current_user,
            )
            for item in payload.items
        ]
        counts = {
            name: sum(item.status == name for item in results)
            for name in (
                "succeeded",
                "replayed",
                "conflict",
                "blocked",
                "forbidden",
                "failed",
            )
        }
        success_count = counts["succeeded"] + counts["replayed"]
        overall = (
            "succeeded"
            if success_count == len(results)
            else "failed"
            if success_count == 0
            else "partial"
        )
        return ApprovalTaskBatchApproveRead(
            batch_request_id=payload.batch_request_id,
            status=overall,
            succeeded_count=counts["succeeded"],
            replayed_count=counts["replayed"],
            conflict_count=counts["conflict"],
            blocked_count=counts["blocked"],
            forbidden_count=counts["forbidden"],
            failed_count=counts["failed"],
            items=results,
        )

    def _approve_one(
        self,
        *,
        item,
        batch_request_id: str,
        current_user: CurrentUserContext,
    ) -> ApprovalTaskBatchItemRead:
        request_id = _item_request_id(batch_request_id, item.task_id)
        tenant = ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)
        db = self.session_factory()
        claim_id: str | None = None
        try:
            task = db.scalar(
                select(ApprovalTask).where(
                    ApprovalTask.tenant_id == tenant,
                    ApprovalTask.id == item.task_id,
                )
            )
            if task is None:
                return self._result(
                    item.task_id,
                    None,
                    "conflict",
                    "TASK_NOT_FOUND",
                    "任务不存在或已移出当前租户。",
                )
            claim_id = task.claim_id
            if not task.batch_eligible:
                reasons = "；".join(task.batch_block_reasons_json or [])
                return self._result(
                    item.task_id,
                    claim_id,
                    "blocked",
                    "TASK_NOT_BATCH_ELIGIBLE",
                    reasons or "该任务不满足安全批量审批条件。",
                )
            actor_id = str(current_user.username or "").strip().casefold()
            replayed = db.scalar(
                select(ApprovalActionLedger.id).where(
                    ApprovalActionLedger.tenant_id == tenant,
                    ApprovalActionLedger.actor_id == actor_id,
                    ApprovalActionLedger.request_id == request_id,
                    ApprovalActionLedger.completed_at.is_not(None),
                )
            ) is not None
            claim = ExpenseClaimService(db).approve_claim(
                task.claim_id,
                current_user,
                opinion=item.opinion,
                request_id=request_id,
                expected_status=item.expected_status,
                expected_approval_stage=item.expected_approval_stage,
                task_id=task.id,
                expected_task_version=item.expected_task_version,
            )
            if claim is None:
                return self._result(
                    item.task_id,
                    claim_id,
                    "conflict",
                    "CLAIM_NOT_FOUND",
                    "关联单据不存在或已不可见。",
                )
            claim_read = (
                claim
                if isinstance(claim, ExpenseClaimRead)
                else ExpenseClaimRead.model_validate(claim)
            )
            return self._result(
                item.task_id,
                claim_id,
                "replayed" if replayed else "succeeded",
                "REPLAYED" if replayed else "APPROVED",
                "审批结果已安全重放。" if replayed else "审批通过。",
                claim=claim_read.model_dump(mode="json"),
            )
        except ApprovalTaskPermissionError as error:
            return self._result(item.task_id, claim_id, "forbidden", "FORBIDDEN", str(error))
        except ExpenseClaimRiskBlockedError as error:
            return self._result(item.task_id, claim_id, "blocked", "OPEN_HIGH_RISK", str(error))
        except (ApprovalActionConflictError, ApprovalTaskConflictError) as error:
            return self._result(item.task_id, claim_id, "conflict", "CONCURRENT_CHANGE", str(error))
        except ValueError as error:
            return self._result(
                item.task_id,
                claim_id,
                "blocked",
                "VALIDATION_BLOCKED",
                str(error),
            )
        except Exception:
            return self._result(
                item.task_id,
                claim_id,
                "failed",
                "INTERNAL_ERROR",
                "该任务处理失败，请稍后单独重试。",
            )
        finally:
            db.rollback()
            db.close()

    @staticmethod
    def _result(
        task_id: str,
        claim_id: str | None,
        status: str,
        code: str,
        message: str,
        *,
        claim: dict | None = None,
    ) -> ApprovalTaskBatchItemRead:
        return ApprovalTaskBatchItemRead(
            task_id=task_id,
            claim_id=claim_id,
            status=status,
            code=code,
            message=message,
            claim=claim,
        )


def _item_request_id(batch_request_id: str, task_id: str) -> str:
    value = f"batch:{batch_request_id}:{task_id}"
    if len(value) <= 120:
        return value
    digest = hashlib.sha256(value.encode("utf-8")).hexdigest()
    return f"batch:{digest}"
