from __future__ import annotations

from collections import defaultdict
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from time import perf_counter
from typing import Any

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

from app.core.agent_enums import (
    AgentName,
    AgentPermissionLevel,
    AgentRunSource,
    AgentRunStatus,
    AgentToolType,
)
from app.models.budget import BudgetAllocation
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.models.role import Role
from app.services.agent_runs import AgentRunService
from app.services.tenant_registry import required_tenant_id

DIGITAL_EMPLOYEE_REMINDER_TASK_TYPE = "digital_employee_reminder_scan"
DIGITAL_EMPLOYEE_REMINDER_TOOL_NAME = "digital_employee.reminder.scan"

APPROVAL_PENDING_STATUSES = {"submitted", "review", "in_progress", "pending", "pending_review"}
PAYMENT_PENDING_STATUSES = {"approved", "pending_payment", "payment_pending"}
ARCHIVE_PENDING_STATUSES = {"paid", "payment_completed", "pending_archive"}
SUPPLEMENT_STATUSES = {"returned", "rejected", "supplement", "supplement_required"}
APPLICATION_ACTIVE_STATUSES = {"approved", "submitted", "review", "in_progress", "pending"}
HIGH_AMOUNT_THRESHOLD = Decimal("10000.00")
DEFAULT_WINDOW_DAYS = 14


class DigitalEmployeeReminderTaskService:
    def __init__(self, db: Session, *, tenant_id: str) -> None:
        self.db = db
        self.tenant_id = required_tenant_id(tenant_id)

    def refresh_reminders(
        self,
        *,
        source: str = AgentRunSource.SCHEDULE.value,
        now: datetime | None = None,
        window_days: int = DEFAULT_WINDOW_DAYS,
    ) -> dict[str, Any]:
        run_service = AgentRunService(self.db)
        started_at = now or datetime.now(UTC)
        run = run_service.create_run(
            agent=AgentName.HERMES.value,
            source=source,
            tenant_id=self.tenant_id,
            user_id="digital_employee",
            ontology_json={"scenario": "financial_reminder", "intent": "scan"},
            route_json={
                "task_type": DIGITAL_EMPLOYEE_REMINDER_TASK_TYPE,
                "job_type": DIGITAL_EMPLOYEE_REMINDER_TASK_TYPE,
                "selected_agent": AgentName.HERMES.value,
                "phase": "running",
                "window_days": int(window_days or DEFAULT_WINDOW_DAYS),
                "heartbeat_at": datetime.now(UTC).isoformat(),
            },
            permission_level=AgentPermissionLevel.READ.value,
            status=AgentRunStatus.RUNNING.value,
            started_at=started_at,
        )
        timer = perf_counter()
        try:
            report = self.build_reminder_report(now=started_at, window_days=window_days)
            summary = self._build_summary(report)
            duration_ms = int((perf_counter() - timer) * 1000)
            response = {
                "task_type": DIGITAL_EMPLOYEE_REMINDER_TASK_TYPE,
                "summary": summary,
                "report": report,
            }
            run_service.record_tool_call(
                run_id=run.run_id,
                tool_type=AgentToolType.DATABASE.value,
                tool_name=DIGITAL_EMPLOYEE_REMINDER_TOOL_NAME,
                request_json={
                    "task_type": DIGITAL_EMPLOYEE_REMINDER_TASK_TYPE,
                    "window_days": int(window_days or DEFAULT_WINDOW_DAYS),
                },
                response_json=response,
                status=AgentRunStatus.SUCCEEDED.value,
                duration_ms=duration_ms,
            )
            run_service.merge_route_json(
                run.run_id,
                {
                    "phase": "succeeded",
                    "summary": summary,
                    "report": report,
                    "heartbeat_at": datetime.now(UTC).isoformat(),
                },
                status=AgentRunStatus.SUCCEEDED.value,
                result_summary=(
                    "定时提醒扫描完成："
                    f"提醒 {summary['recipient_count']} 人，"
                    f"生成 {summary['reminder_count']} 条事项。"
                ),
                finished_at=datetime.now(UTC),
            )
            return response
        except Exception as exc:
            run_service.record_tool_call(
                run_id=run.run_id,
                tool_type=AgentToolType.DATABASE.value,
                tool_name=DIGITAL_EMPLOYEE_REMINDER_TOOL_NAME,
                request_json={"task_type": DIGITAL_EMPLOYEE_REMINDER_TASK_TYPE},
                response_json={},
                status=AgentRunStatus.FAILED.value,
                duration_ms=int((perf_counter() - timer) * 1000),
                error_message=str(exc),
            )
            run_service.merge_route_json(
                run.run_id,
                {"phase": "failed", "heartbeat_at": datetime.now(UTC).isoformat()},
                status=AgentRunStatus.FAILED.value,
                error_message=str(exc),
                finished_at=datetime.now(UTC),
            )
            raise

    def build_reminder_report(
        self,
        *,
        now: datetime | None = None,
        window_days: int = DEFAULT_WINDOW_DAYS,
    ) -> dict[str, Any]:
        scan_time = self._aware(now or datetime.now(UTC))
        recipient_map: dict[str, dict[str, Any]] = {}
        counters: dict[str, int] = defaultdict(int)

        for reminder in [
            *self._approval_pending_reminders(scan_time),
            *self._budget_compilation_reminders(scan_time),
            *self._travel_application_expiry_reminders(scan_time, window_days=window_days),
            *self._reimbursement_overdue_reminders(scan_time, window_days=window_days),
        ]:
            self._append_reminder(recipient_map, reminder)
            counters[str(reminder["type"])] += 1

        recipients = sorted(
            recipient_map.values(),
            key=lambda item: (-len(item["reminders"]), item["recipientName"]),
        )
        return {
            "title": "数字员工定时提醒扫描报告",
            "generatedAt": scan_time.isoformat(),
            "windowDays": int(window_days or DEFAULT_WINDOW_DAYS),
            "totals": {
                "recipientCount": len(recipients),
                "reminderCount": sum(counters.values()),
                "approvalPendingCount": counters["approval_pending"],
                "budgetReminderCount": counters["budget_compilation"],
                "travelApplicationReminderCount": counters["travel_application_expiry"],
                "reimbursementOverdueCount": counters["reimbursement_overdue"],
            },
            "recipients": recipients,
        }

    def _approval_pending_reminders(self, now: datetime) -> list[dict[str, Any]]:
        stmt = (
            select(ExpenseClaim)
            .options(selectinload(ExpenseClaim.employee).selectinload(Employee.manager))
            .where(
                ExpenseClaim.tenant_id == self.tenant_id,
                ExpenseClaim.status.in_(APPROVAL_PENDING_STATUSES),
            )
            .order_by(ExpenseClaim.submitted_at.asc().nullslast(), ExpenseClaim.updated_at.asc())
            .limit(200)
        )
        reminders: list[dict[str, Any]] = []
        for claim in self.db.scalars(stmt).all():
            recipient = self._approval_recipient(claim)
            wait_started_at = claim.submitted_at or claim.updated_at or claim.created_at
            wait_days = self._wait_days(now, wait_started_at)
            reminders.append(
                self._document_reminder(
                    reminder_type="approval_pending",
                    recipient=recipient,
                    claim=claim,
                    title=f"{claim.claim_no} 待审批",
                    action="请在今日处理审批待办，避免影响后续付款和归档。",
                    wait_days=wait_days,
                    type_score=0.85,
                )
            )
        return reminders

    def _budget_compilation_reminders(self, now: datetime) -> list[dict[str, Any]]:
        fiscal_year = now.astimezone(UTC).year
        period_key = self._current_quarter_key(now)
        active_statuses = {"active", "published"}
        year_count = (
            self.db.scalar(
                select(func.count(BudgetAllocation.id)).where(
                    BudgetAllocation.fiscal_year == fiscal_year,
                    BudgetAllocation.status.in_(active_statuses),
                )
            )
            or 0
        )
        period_count = (
            self.db.scalar(
                select(func.count(BudgetAllocation.id)).where(
                    BudgetAllocation.fiscal_year == fiscal_year,
                    BudgetAllocation.period_key == period_key,
                    BudgetAllocation.status.in_(active_statuses),
                )
            )
            or 0
        )
        if year_count and period_count:
            return []

        recipients = self._budget_admin_recipients()
        if not recipients:
            recipients = [
                {
                    "recipientId": "budget_admin",
                    "recipientName": "预算管理员",
                    "recipientRole": "budget_admin",
                }
            ]
        title = (
            f"{fiscal_year} 年预算池待建立"
            if not year_count
            else f"{fiscal_year} 年 {period_key} 预算池待补齐"
        )
        return [
            {
                "type": "budget_compilation",
                "priority": "high" if not year_count else "medium",
                "priorityScore": 0.9 if not year_count else 0.65,
                "title": title,
                "action": "请检查预算编制进度，补齐部门、费用类型和期间预算池。",
                "recipient": recipient,
                "relatedDocuments": [],
                "metrics": {
                    "fiscalYear": fiscal_year,
                    "periodKey": period_key,
                    "activeYearAllocationCount": int(year_count),
                    "activePeriodAllocationCount": int(period_count),
                },
            }
            for recipient in recipients
        ]

    def _travel_application_expiry_reminders(
        self,
        now: datetime,
        *,
        window_days: int,
    ) -> list[dict[str, Any]]:
        cutoff = now - timedelta(days=max(1, int(window_days or DEFAULT_WINDOW_DAYS)))
        stmt = (
            select(ExpenseClaim)
            .options(selectinload(ExpenseClaim.employee))
            .where(ExpenseClaim.tenant_id == self.tenant_id)
            .where(ExpenseClaim.expense_type.like("%_application"))
            .where(ExpenseClaim.status.in_(APPLICATION_ACTIVE_STATUSES))
            .where(ExpenseClaim.occurred_at <= now)
            .where(ExpenseClaim.occurred_at >= cutoff)
            .order_by(ExpenseClaim.occurred_at.asc())
            .limit(200)
        )
        reminders: list[dict[str, Any]] = []
        for claim in self.db.scalars(stmt).all():
            if not self._is_travel_application(claim):
                continue
            if self._has_linked_reimbursement_draft(claim):
                continue
            wait_days = self._wait_days(now, claim.occurred_at)
            reminders.append(
                self._document_reminder(
                    reminder_type="travel_application_expiry",
                    recipient=self._employee_recipient(claim),
                    claim=claim,
                    title=f"{claim.claim_no} 出差申请已到期",
                    action="请发起报销、延长申请或关闭未使用申请。",
                    wait_days=wait_days,
                    type_score=0.75,
                )
            )
        return reminders

    def _reimbursement_overdue_reminders(
        self,
        now: datetime,
        *,
        window_days: int,
    ) -> list[dict[str, Any]]:
        cutoff = now - timedelta(days=max(1, int(window_days or DEFAULT_WINDOW_DAYS)))
        statuses = PAYMENT_PENDING_STATUSES | ARCHIVE_PENDING_STATUSES | SUPPLEMENT_STATUSES
        stmt = (
            select(ExpenseClaim)
            .options(selectinload(ExpenseClaim.employee))
            .where(ExpenseClaim.tenant_id == self.tenant_id)
            .where(ExpenseClaim.status.in_(statuses))
            .where(ExpenseClaim.updated_at >= cutoff)
            .order_by(ExpenseClaim.updated_at.asc())
            .limit(200)
        )
        reminders: list[dict[str, Any]] = []
        for claim in self.db.scalars(stmt).all():
            if self._is_application_claim(claim):
                continue
            status = str(claim.status or "").strip()
            recipient = self._finance_recipient(claim)
            action = "请检查付款或归档处理进度。"
            title = f"{claim.claim_no} 报销流程待处理"
            if status in SUPPLEMENT_STATUSES:
                recipient = self._employee_recipient(claim)
                title = f"{claim.claim_no} 待补充材料"
                action = "请补齐材料后重新提交，减少财务反复沟通。"
            elif status in PAYMENT_PENDING_STATUSES:
                title = f"{claim.claim_no} 待付款"
                action = "请确认付款排期，避免已审批单据长期停留。"
            elif status in ARCHIVE_PENDING_STATUSES:
                title = f"{claim.claim_no} 待归档"
                action = "请完成归档，保证单据闭环和后续审计可追踪。"
            wait_started_at = claim.updated_at or claim.submitted_at or claim.created_at
            wait_days = self._wait_days(now, wait_started_at)
            reminders.append(
                self._document_reminder(
                    reminder_type="reimbursement_overdue",
                    recipient=recipient,
                    claim=claim,
                    title=title,
                    action=action,
                    wait_days=wait_days,
                    type_score=0.7,
                )
            )
        return reminders

    def _document_reminder(
        self,
        *,
        reminder_type: str,
        recipient: dict[str, str],
        claim: ExpenseClaim,
        title: str,
        action: str,
        wait_days: int,
        type_score: float,
    ) -> dict[str, Any]:
        amount = Decimal(claim.amount or 0)
        priority_score = self._priority_score(
            wait_days=wait_days,
            amount=amount,
            type_score=type_score,
        )
        return {
            "type": reminder_type,
            "priority": self._priority(priority_score),
            "priorityScore": round(priority_score, 4),
            "title": title,
            "action": action,
            "recipient": recipient,
            "relatedDocuments": [
                {
                    "documentId": claim.id,
                    "documentNo": claim.claim_no,
                    "employeeName": claim.employee_name,
                    "departmentName": claim.department_name,
                    "expenseType": claim.expense_type,
                    "status": claim.status,
                    "approvalStage": claim.approval_stage,
                    "amount": float(amount),
                    "waitDays": wait_days,
                }
            ],
            "metrics": {
                "amount": float(amount),
                "waitDays": wait_days,
            },
        }

    @staticmethod
    def _append_reminder(
        recipient_map: dict[str, dict[str, Any]],
        reminder: dict[str, Any],
    ) -> None:
        recipient = dict(reminder.pop("recipient"))
        recipient_id = str(
            recipient.get("recipientId") or recipient.get("recipientName") or "unknown"
        )
        row = recipient_map.setdefault(
            recipient_id,
            {
                "recipientId": recipient_id,
                "recipientName": str(recipient.get("recipientName") or recipient_id),
                "recipientRole": str(recipient.get("recipientRole") or "unknown"),
                "reminders": [],
            },
        )
        row["reminders"].append(reminder)

    def _approval_recipient(self, claim: ExpenseClaim) -> dict[str, str]:
        employee = claim.employee
        if employee is not None and employee.manager is not None:
            return {
                "recipientId": employee.manager.id,
                "recipientName": employee.manager.name,
                "recipientRole": "manager",
            }
        return self._finance_recipient(claim)

    @staticmethod
    def _employee_recipient(claim: ExpenseClaim) -> dict[str, str]:
        if claim.employee is not None:
            return {
                "recipientId": claim.employee.id,
                "recipientName": claim.employee.name,
                "recipientRole": "employee",
            }
        return {
            "recipientId": str(claim.employee_id or claim.employee_name or "employee"),
            "recipientName": str(claim.employee_name or "员工"),
            "recipientRole": "employee",
        }

    @staticmethod
    def _finance_recipient(claim: ExpenseClaim) -> dict[str, str]:
        employee = claim.employee
        owner = ""
        if employee is not None:
            owner = str(employee.finance_owner_name or "").strip()
        return {
            "recipientId": owner or "finance_operator",
            "recipientName": owner or "财务经办人",
            "recipientRole": "finance",
        }

    def _budget_admin_recipients(self) -> list[dict[str, str]]:
        stmt = (
            select(Employee)
            .options(selectinload(Employee.roles))
            .join(Employee.roles)
            .where(
                Employee.tenant_id == self.tenant_id,
                Role.role_code.in_(("budget_monitor", "executive")),
            )
            .order_by(Employee.name.asc())
            .limit(20)
        )
        recipients = []
        seen: set[str] = set()
        for employee in self.db.scalars(stmt).all():
            if employee.id in seen:
                continue
            seen.add(employee.id)
            recipients.append(
                {
                    "recipientId": employee.id,
                    "recipientName": employee.name,
                    "recipientRole": "budget_admin",
                }
            )
        return recipients

    @staticmethod
    def _is_travel_application(claim: ExpenseClaim) -> bool:
        expense_type = str(claim.expense_type or "").strip().lower()
        if expense_type == "travel_application":
            return True
        for flag in list(claim.risk_flags_json or []):
            if not isinstance(flag, dict):
                continue
            detail = flag.get("application_detail") or flag.get("applicationDetail") or {}
            if isinstance(detail, dict) and "差旅" in str(detail.get("application_type") or ""):
                return True
        return False

    @staticmethod
    def _is_application_claim(claim: ExpenseClaim) -> bool:
        expense_type = str(claim.expense_type or "").strip().lower()
        if expense_type in {"application", "expense_application"} or expense_type.endswith(
            "_application"
        ):
            return True
        for flag in list(claim.risk_flags_json or []):
            if not isinstance(flag, dict):
                continue
            if str(flag.get("business_stage") or "").strip() == "expense_application":
                return True
            if isinstance(flag.get("application_detail"), dict):
                return True
        return False

    def _has_linked_reimbursement_draft(self, application_claim: ExpenseClaim) -> bool:
        for flag in list(application_claim.risk_flags_json or []):
            if not isinstance(flag, dict):
                continue
            if flag.get("generated_draft_claim_id") or flag.get("generated_draft_claim_no"):
                return True
        stmt = (
            select(ExpenseClaim)
            .where(
                ExpenseClaim.tenant_id == self.tenant_id,
                ExpenseClaim.expense_type.not_like("%_application"),
            )
            .order_by(ExpenseClaim.created_at.desc())
            .limit(300)
        )
        for claim in self.db.scalars(stmt).all():
            for flag in list(claim.risk_flags_json or []):
                if not isinstance(flag, dict):
                    continue
                if str(flag.get("application_claim_id") or "") == application_claim.id:
                    return True
        return False

    @staticmethod
    def _priority_score(*, wait_days: int, amount: Decimal, type_score: float) -> float:
        wait_score = min(max(wait_days, 0) / 3, 1)
        amount_score = min(float(max(amount, Decimal("0.00")) / HIGH_AMOUNT_THRESHOLD), 1)
        return 0.45 * wait_score + 0.35 * amount_score + 0.20 * type_score

    @staticmethod
    def _priority(score: float) -> str:
        if score >= 0.75:
            return "high"
        if score >= 0.45:
            return "medium"
        return "low"

    @classmethod
    def _wait_days(cls, now: datetime, started_at: datetime | None) -> int:
        if started_at is None:
            return 0
        delta = cls._aware(now) - cls._aware(started_at)
        return max(0, int(delta.total_seconds() // 86400))

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

    @staticmethod
    def _current_quarter_key(now: datetime) -> str:
        month = now.month
        quarter = ((month - 1) // 3) + 1
        return f"Q{quarter}"

    @staticmethod
    def _build_summary(report: dict[str, Any]) -> dict[str, Any]:
        totals = report.get("totals") if isinstance(report, dict) else {}
        totals = totals if isinstance(totals, dict) else {}
        return {
            "recipient_count": int(totals.get("recipientCount") or 0),
            "reminder_count": int(totals.get("reminderCount") or 0),
            "approval_pending_count": int(totals.get("approvalPendingCount") or 0),
            "budget_reminder_count": int(totals.get("budgetReminderCount") or 0),
            "travel_application_reminder_count": int(
                totals.get("travelApplicationReminderCount") or 0
            ),
            "reimbursement_overdue_count": int(totals.get("reimbursementOverdueCount") or 0),
        }
