from __future__ import annotations

import json
import math
from dataclasses import dataclass, replace
from datetime import UTC, datetime, timedelta

from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.entitlement import EntitlementPlan, UserEntitlement, UserEntitlementLog
from app.models.user import User


DEFAULT_PLAN_TYPE = "basic"


@dataclass(frozen=True)
class EntitlementView:
    plan_id: int | None
    name: str
    plan_type: str
    description: str | None
    validity_days: int | None
    monthly_topic_limit: int | None
    monthly_topic_used: int
    enable_growth_profile: bool
    enable_periodic_reports: bool
    allow_help_card: bool
    allow_share_draft: bool
    deduct_quota: bool
    effective_at: datetime | None = None
    expired_at: datetime | None = None
    source: str = "legacy"
    lifecycle_status: str = "legacy"
    days_until_expiry: int | None = None
    previous_plan_name: str | None = None
    previous_expired_at: datetime | None = None

    @property
    def monthly_topic_remaining(self) -> int | None:
        if self.monthly_topic_limit is None:
            return None
        return max(0, self.monthly_topic_limit - self.monthly_topic_used)


class EntitlementService:
    @staticmethod
    def list_plans(db: Session, *, include_disabled: bool = False) -> list[EntitlementPlan]:
        query = select(EntitlementPlan).where(EntitlementPlan.plan_type != "teacher")
        if not include_disabled:
            query = query.where(EntitlementPlan.status == 1)
        return list(db.scalars(query.order_by(EntitlementPlan.sort_order.asc(), EntitlementPlan.id.asc())))

    @staticmethod
    def default_plan(db: Session) -> EntitlementPlan | None:
        plan = db.scalar(
            select(EntitlementPlan)
            .where(
                EntitlementPlan.plan_type == DEFAULT_PLAN_TYPE,
                EntitlementPlan.plan_type != "teacher",
                EntitlementPlan.status == 1,
            )
            .order_by(EntitlementPlan.sort_order.asc(), EntitlementPlan.id.asc())
            .limit(1)
        )
        if plan is not None:
            return plan
        return db.scalar(
            select(EntitlementPlan)
            .where(EntitlementPlan.status == 1, EntitlementPlan.plan_type != "teacher")
            .order_by(EntitlementPlan.sort_order.asc(), EntitlementPlan.id.asc())
            .limit(1)
        )

    @staticmethod
    def active_entitlement(db: Session, user: User, *, monthly_topic_used: int = 0) -> EntitlementView:
        now = _now()
        row = db.execute(
            select(UserEntitlement, EntitlementPlan)
            .join(EntitlementPlan, EntitlementPlan.id == UserEntitlement.plan_id)
            .where(
                UserEntitlement.user_id == user.id,
                UserEntitlement.status == "active",
                EntitlementPlan.status == 1,
                EntitlementPlan.plan_type != "teacher",
            )
            .where((UserEntitlement.effective_at.is_(None)) | (UserEntitlement.effective_at <= now))
            .where((UserEntitlement.expired_at.is_(None)) | (UserEntitlement.expired_at >= now))
            .order_by(UserEntitlement.created_at.desc(), UserEntitlement.id.desc())
            .limit(1)
        ).first()
        if row:
            entitlement, plan = row
            return view_from_plan(plan, monthly_topic_used=monthly_topic_used, entitlement=entitlement, source="assigned")

        plan = EntitlementService.default_plan(db)
        if plan is not None:
            previous = db.execute(
                select(UserEntitlement, EntitlementPlan)
                .join(EntitlementPlan, EntitlementPlan.id == UserEntitlement.plan_id)
                .where(
                    UserEntitlement.user_id == user.id,
                    UserEntitlement.status.in_(("active", "expired")),
                    UserEntitlement.expired_at.is_not(None),
                    UserEntitlement.expired_at < now,
                    EntitlementPlan.plan_type != "teacher",
                )
                .order_by(UserEntitlement.expired_at.desc(), UserEntitlement.id.desc())
                .limit(1)
            ).first()
            view = view_from_plan(plan, monthly_topic_used=monthly_topic_used, entitlement=None, source="default")
            if previous:
                expired_entitlement, expired_plan = previous
                return replace(
                    view,
                    source="default_after_expiry",
                    lifecycle_status="expired_fallback",
                    previous_plan_name=expired_plan.name,
                    previous_expired_at=expired_entitlement.expired_at,
                )
            return view

        return EntitlementView(
            plan_id=None,
            name="旧版每日额度",
            plan_type="legacy",
            description="按每日问答额度提供基础服务。",
            validity_days=None,
            monthly_topic_limit=None,
            monthly_topic_used=monthly_topic_used,
            enable_growth_profile=False,
            enable_periodic_reports=False,
            allow_help_card=True,
            allow_share_draft=True,
            deduct_quota=True,
            source="legacy",
            lifecycle_status="legacy",
        )

    @staticmethod
    def expire_due_entitlements(db: Session, *, batch_size: int = 500, now: datetime | None = None) -> int:
        current_time = now or _now()
        total = 0
        while True:
            due = list(
                db.scalars(
                    select(UserEntitlement)
                    .where(
                        UserEntitlement.status == "active",
                        UserEntitlement.expired_at.is_not(None),
                        UserEntitlement.expired_at < current_time,
                    )
                    .order_by(UserEntitlement.id.asc())
                    .limit(batch_size)
                )
            )
            if not due:
                return total
            for entitlement in due:
                entitlement.status = "expired"
                db.add(entitlement)
                db.add(
                    UserEntitlementLog(
                        user_id=entitlement.user_id,
                        entitlement_id=entitlement.id,
                        from_plan_id=entitlement.plan_id,
                        to_plan_id=None,
                        action="expire",
                        detail_json=json.dumps(
                            {"expiredAt": entitlement.expired_at.isoformat() if entitlement.expired_at else None},
                            ensure_ascii=False,
                        ),
                        operated_by=None,
                        created_at=current_time,
                    )
                )
            db.flush()
            total += len(due)

    @staticmethod
    def renew_user_plan(
        db: Session,
        *,
        user: User,
        extension_days: int,
        request_key: str,
        operated_by: int | None,
        remark: str | None = None,
    ) -> UserEntitlement:
        existing_log = db.scalar(select(UserEntitlementLog).where(UserEntitlementLog.request_key == request_key))
        if existing_log is not None and existing_log.entitlement_id is not None:
            existing = db.get(UserEntitlement, existing_log.entitlement_id)
            if existing is not None:
                return existing

        db.scalar(select(User.id).where(User.id == user.id).with_for_update())
        existing_log = db.scalar(select(UserEntitlementLog).where(UserEntitlementLog.request_key == request_key))
        if existing_log is not None and existing_log.entitlement_id is not None:
            existing = db.get(UserEntitlement, existing_log.entitlement_id)
            if existing is not None:
                return existing

        now = _now()
        current = db.scalar(
            select(UserEntitlement)
            .where(UserEntitlement.user_id == user.id, UserEntitlement.status.in_(("active", "expired")))
            .order_by(UserEntitlement.created_at.desc(), UserEntitlement.id.desc())
            .limit(1)
        )
        if current is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="该用户尚未分配专属权益，请先选择权益版本")
        plan = db.get(EntitlementPlan, current.plan_id)
        if plan is None or plan.status != 1 or plan.plan_type == "teacher":
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="原权益版本已停用，无法续期，请重新分配权益")
        if current.status == "active" and current.expired_at is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前权益为长期有效，无需续期")

        previous_expired_at = current.expired_at
        base_time = max(now, previous_expired_at) if previous_expired_at is not None else now
        renewed_expired_at = base_time + timedelta(days=extension_days)
        current.status = "replaced"
        db.add(current)
        renewed = UserEntitlement(
            user_id=user.id,
            plan_id=plan.id,
            status="active",
            effective_at=now,
            expired_at=renewed_expired_at,
            assigned_by=operated_by,
            remark=remark,
        )
        db.add(renewed)
        db.flush()
        db.add(
            UserEntitlementLog(
                user_id=user.id,
                entitlement_id=renewed.id,
                from_plan_id=plan.id,
                to_plan_id=plan.id,
                action="renew",
                request_key=request_key,
                detail_json=json.dumps(
                    {
                        "extensionDays": extension_days,
                        "previousExpiredAt": previous_expired_at.isoformat() if previous_expired_at else None,
                        "renewedExpiredAt": renewed_expired_at.isoformat(),
                        "sourceEntitlementId": current.id,
                        "remark": remark,
                    },
                    ensure_ascii=False,
                ),
                operated_by=operated_by,
                created_at=now,
            )
        )
        return renewed

    @staticmethod
    def assign_user_plan(
        db: Session,
        *,
        user: User,
        plan_id: int,
        operated_by: int | None,
        effective_at: datetime | None = None,
        expired_at: datetime | None = None,
        remark: str | None = None,
    ) -> UserEntitlement:
        plan = db.get(EntitlementPlan, plan_id)
        if plan is None or plan.status != 1 or plan.plan_type == "teacher":
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="权益版本不存在或已停用")
        now = _now()
        if effective_at is not None:
            effective_at = effective_at.replace(tzinfo=None)
        if expired_at is None and plan.validity_days:
            start = effective_at or now
            expired_at = start + timedelta(days=plan.validity_days)
        elif expired_at is not None:
            expired_at = expired_at.replace(tzinfo=None)
        if expired_at is not None and effective_at is not None and expired_at < effective_at:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="权益到期时间不能早于生效时间")

        current = db.scalar(
            select(UserEntitlement)
            .where(UserEntitlement.user_id == user.id, UserEntitlement.status == "active")
            .order_by(UserEntitlement.created_at.desc(), UserEntitlement.id.desc())
            .limit(1)
        )
        from_plan_id = current.plan_id if current else None
        if current is not None:
            current.status = "replaced"
            db.add(current)

        entitlement = UserEntitlement(
            user_id=user.id,
            plan_id=plan.id,
            status="active",
            effective_at=effective_at,
            expired_at=expired_at,
            assigned_by=operated_by,
            remark=remark,
        )
        db.add(entitlement)
        db.flush()
        db.add(
            UserEntitlementLog(
                user_id=user.id,
                entitlement_id=entitlement.id,
                from_plan_id=from_plan_id,
                to_plan_id=plan.id,
                action="assign",
                detail_json=json.dumps(
                    {
                        "effectiveAt": effective_at.isoformat() if effective_at else None,
                        "expiredAt": expired_at.isoformat() if expired_at else None,
                        "remark": remark,
                    },
                    ensure_ascii=False,
                ),
                operated_by=operated_by,
                created_at=now,
            )
        )
        return entitlement


def plan_dict(plan: EntitlementPlan) -> dict:
    return {
        "id": plan.id,
        "name": plan.name,
        "planType": plan.plan_type,
        "description": plan.description,
        "validityDays": plan.validity_days,
        "monthlyTopicLimit": plan.monthly_topic_limit,
        "enableGrowthProfile": bool(plan.enable_growth_profile),
        "enablePeriodicReports": bool(plan.enable_periodic_reports),
        "allowHelpCard": bool(plan.allow_help_card),
        "allowShareDraft": bool(plan.allow_share_draft),
        "deductQuota": bool(plan.deduct_quota),
        "status": plan.status,
        "sortOrder": plan.sort_order,
        "createdAt": plan.created_at,
        "updatedAt": plan.updated_at,
    }


def entitlement_dict(view: EntitlementView) -> dict:
    return {
        "planId": view.plan_id,
        "name": view.name,
        "planType": view.plan_type,
        "description": view.description,
        "validityDays": view.validity_days,
        "monthlyTopicLimit": view.monthly_topic_limit,
        "monthlyTopicUsed": view.monthly_topic_used,
        "monthlyTopicRemaining": view.monthly_topic_remaining,
        "enableGrowthProfile": view.enable_growth_profile,
        "enablePeriodicReports": view.enable_periodic_reports,
        "allowHelpCard": view.allow_help_card,
        "allowShareDraft": view.allow_share_draft,
        "deductQuota": view.deduct_quota,
        "effectiveAt": view.effective_at,
        "expiredAt": view.expired_at,
        "source": view.source,
        "lifecycleStatus": view.lifecycle_status,
        "daysUntilExpiry": view.days_until_expiry,
        "previousPlanName": view.previous_plan_name,
        "previousExpiredAt": view.previous_expired_at,
    }


def entitlement_prompt_context(view: EntitlementView) -> str:
    """Describe product capabilities without letting the model pretend to execute them."""
    help_card = "可由学员主动生成" if view.allow_help_card else "当前权益不可生成"
    share_draft = "可由学员主动生成" if view.allow_share_draft else "当前权益不可生成"
    return (
        "[当前产品权益]\n"
        f"权益名称：{view.name}；权益类型：{view.plan_type}。\n"
        f"老师求助卡：{help_card}；班级分享稿：{share_draft}。\n"
        "这些能力由页面按钮和专用接口真实执行。不得声称已自动转人工、已联系老师、已生成或已发送卡片；"
        "只有学员明确询问相关能力时，才说明当前是否可以由学员主动生成。"
    )


def view_from_plan(
    plan: EntitlementPlan,
    *,
    monthly_topic_used: int,
    entitlement: UserEntitlement | None,
    source: str,
) -> EntitlementView:
    days_until_expiry = _days_until_expiry(entitlement.expired_at) if entitlement else None
    lifecycle_status = "default"
    if entitlement is not None:
        if entitlement.expired_at is None:
            lifecycle_status = "long_term"
        elif days_until_expiry is not None and days_until_expiry <= 7:
            lifecycle_status = "expiring_7"
        elif days_until_expiry is not None and days_until_expiry <= 30:
            lifecycle_status = "expiring_30"
        else:
            lifecycle_status = "active"
    return EntitlementView(
        plan_id=plan.id,
        name=plan.name,
        plan_type=plan.plan_type,
        description=plan.description,
        validity_days=plan.validity_days,
        monthly_topic_limit=plan.monthly_topic_limit,
        monthly_topic_used=monthly_topic_used,
        enable_growth_profile=bool(plan.enable_growth_profile),
        enable_periodic_reports=bool(plan.enable_periodic_reports),
        allow_help_card=bool(plan.allow_help_card),
        allow_share_draft=bool(plan.allow_share_draft),
        deduct_quota=bool(plan.deduct_quota),
        effective_at=entitlement.effective_at if entitlement else None,
        expired_at=entitlement.expired_at if entitlement else None,
        source=source,
        lifecycle_status=lifecycle_status,
        days_until_expiry=days_until_expiry,
    )


def _now() -> datetime:
    return datetime.now(UTC).replace(tzinfo=None)


def _days_until_expiry(expired_at: datetime | None) -> int | None:
    if expired_at is None:
        return None
    return max(0, math.ceil((expired_at - _now()).total_seconds() / 86400))
