"""自动续期账期签发；订阅行锁保证并发下只滚动一次。"""

from __future__ import annotations

from datetime import UTC, datetime

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.commercial import TenantCommercialPlan, TenantSubscription
from app.schemas.commercial_billing import CommercialRolloverRead
from app.services.commercial_admin_audit import (
    CommercialAdminAuditService,
    snapshot_resource,
)
from app.services.commercial_billing_periods import CommercialBillingPeriodService
from app.services.commercial_periods import add_billing_interval


class CommercialSubscriptionRolloverService:
    def __init__(self, db: Session) -> None:
        self.db = db

    def rollover_due(
        self,
        tenant_id: str,
        subscription_id: str,
        *,
        as_of: datetime | None = None,
        actor_id: str = "commercial-rollover-scheduler",
        max_periods: int = 24,
    ) -> CommercialRolloverRead:
        if not 1 <= max_periods <= 120:
            raise ValueError("单次自动续期账期数量必须介于 1 和 120 之间。")
        when = _utc(as_of or datetime.now(UTC))
        subscription = self.db.scalar(
            select(TenantSubscription)
            .where(
                TenantSubscription.tenant_id == tenant_id.strip(),
                TenantSubscription.id == subscription_id.strip(),
            )
            .with_for_update()
            .execution_options(populate_existing=True)
        )
        if subscription is None:
            raise LookupError("自动续期订阅不存在。")
        ineligible = self._ineligible_reason(subscription)
        if ineligible is not None:
            return self._result(subscription, "ineligible", *ineligible)
        if _utc(subscription.current_period_end) > when:
            return self._result(
                subscription,
                "not_due",
                "period_not_due",
                "当前账期尚未结束。",
            )
        plan = self.db.scalar(
            select(TenantCommercialPlan).where(
                TenantCommercialPlan.tenant_id == subscription.tenant_id,
                TenantCommercialPlan.id == subscription.plan_id,
            )
        )
        if plan is None:
            raise LookupError("自动续期订阅引用的套餐不存在。")

        period_service = CommercialBillingPeriodService(self.db)
        audit = CommercialAdminAuditService(self.db)
        created_period_ids: list[str] = []
        replayed = False
        stop_reason: tuple[str, str] | None = None
        for _ in range(max_periods):
            start = _utc(subscription.current_period_end)
            if start > when:
                break
            contract_end = _optional_utc(subscription.ends_at)
            if contract_end is not None and start >= contract_end:
                stop_reason = (
                    "contract_ended",
                    "订阅合同已经结束，拒绝自动生成合同外账期。",
                )
                break
            end = add_billing_interval(start, subscription.billing_interval)
            if contract_end is not None and end > contract_end:
                stop_reason = (
                    "contract_boundary_requires_renewal",
                    "下一完整账期会越过合同结束时间，必须显式续签。",
                )
                break
            request_id = f"rollover:{subscription.id}:{start.isoformat()}"[:120]
            reason = "自动续期服务在合同边界内签发下一不可变账期。"
            before = snapshot_resource(subscription)
            period, created = period_service.issue(
                subscription,
                plan,
                period_start=start,
                period_end=end,
                source="auto_renew",
                actor_id=actor_id,
                idempotency_key=request_id,
            )
            if created:
                created_period_ids.append(period.id)
            else:
                replayed = True
            audit.record(
                tenant_id=subscription.tenant_id,
                actor_type="system",
                actor_id=actor_id,
                request_id=request_id,
                reason=reason,
                action="billing_period_created",
                resource=period,
                before={},
                after=snapshot_resource(period),
            )
            subscription.current_period_start = start
            subscription.current_period_end = end
            subscription.version += 1
            self.db.flush()
            audit.record(
                tenant_id=subscription.tenant_id,
                actor_type="system",
                actor_id=actor_id,
                request_id=request_id,
                reason=reason,
                action="subscription_rolled_over",
                resource=subscription,
                before=before,
                after=snapshot_resource(subscription),
            )
        if created_period_ids:
            return self._result(
                subscription,
                "rolled_over",
                "periods_created",
                f"已签发 {len(created_period_ids)} 个不可变账期。",
                created_period_ids,
            )
        if replayed:
            return self._result(
                subscription,
                "replayed",
                "period_already_exists",
                "账期已由并发或先前请求签发，订阅投影已对齐。",
            )
        if stop_reason is not None:
            return self._result(subscription, "ineligible", *stop_reason)
        return self._result(
            subscription,
            "not_due",
            "period_not_due",
            "当前账期尚未结束。",
        )

    @staticmethod
    def _ineligible_reason(subscription: TenantSubscription) -> tuple[str, str] | None:
        if subscription.status not in {"trialing", "active"}:
            return "subscription_not_consumable", "只有试用中或生效订阅可以自动续期。"
        if not subscription.auto_renew:
            return "auto_renew_disabled", "订阅未启用自动续期。"
        if subscription.billing_interval == "contract":
            return (
                "contract_interval_requires_explicit_renewal",
                "合同制账期不能自动推导，必须显式续签合同。",
            )
        return None

    @staticmethod
    def _result(
        subscription: TenantSubscription,
        status: str,
        reason_code: str,
        reason: str,
        created_period_ids: list[str] | None = None,
    ) -> CommercialRolloverRead:
        return CommercialRolloverRead(
            tenant_id=subscription.tenant_id,
            subscription_id=subscription.id,
            status=status,
            reason_code=reason_code,
            reason=reason,
            created_period_ids=list(created_period_ids or []),
            current_period_start=subscription.current_period_start,
            current_period_end=subscription.current_period_end,
            subscription_version=subscription.version,
        )


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


def _optional_utc(value: datetime | None) -> datetime | None:
    return _utc(value) if value is not None else None
