import logging
import random
import string
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError, ProgrammingError
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.auto_renewal_event import AutoRenewalEvent
from app.models.balance_log import BalanceChangeType, BalanceLog
from app.models.instance import Instance, InstanceStatus, SuspendType
from app.models.instance_disk_addon import InstanceDiskAddon, InstanceDiskAddonStatus
from app.models.notification import NotificationEvent
from app.models.order import Order, OrderStatus
from app.models.user import User
from app.services import (
    agent_service,
    instance_lifecycle_service,
    membership_service,
    notification_service,
    spend_service,
    zjmf_instance_adapter,
)

log = logging.getLogger("qycw.instance.renewal")

AUTO_RENEW_BEFORE_DAYS = 3
AUTO_RENEW_LIMIT = 100
CN_TZ = timezone(timedelta(hours=8))


class RenewalError(Exception):
    def __init__(self, status_code: int, detail: str):
        super().__init__(detail)
        self.status_code = status_code
        self.detail = detail


class InsufficientBalanceError(RenewalError):
    def __init__(self, required_amount: int, current_balance: int):
        super().__init__(
            402,
            f"余额不足，需要 ¥{required_amount / 100:.2f}，当前余额 ¥{current_balance / 100:.2f}",
        )
        self.required_amount = required_amount
        self.current_balance = current_balance


class PendingDiskTaskError(RenewalError):
    def __init__(self):
        super().__init__(400, "存在未完成的磁盘任务，请等待任务完成后再续费")


class DuplicateAutoRenewalError(RenewalError):
    def __init__(self, event_key: str):
        super().__init__(409, "该到期周期已执行过自动续费")
        self.event_key = event_key


@dataclass(frozen=True)
class RenewalResult:
    order_id: int
    order_no: str
    amount: int
    balance: int
    expires_at: datetime
    unsuspend_taskid: int | str | None = None


def _aware(value: datetime | None) -> datetime | None:
    if value is None:
        return None
    if value.tzinfo is None:
        return value.replace(tzinfo=timezone.utc)
    return value


def _format_expiry_time(value: datetime | None) -> str:
    if not value:
        return "-"
    return value.astimezone(CN_TZ).strftime("%Y/%m/%d %H:%M")


def _format_money(cents: int) -> str:
    return f"¥{cents / 100:.2f}"


def _instance_name(instance: Instance) -> str:
    name = instance.alias or instance.hostname
    return f"{name}（实例 #{instance.id}）" if name else f"实例 #{instance.id}"


def _instance_base_renew_amount(instance: Instance) -> int:
    return int(instance.unit_price if instance.unit_price is not None else instance.next_renew_amount or 0)


def _gen_renew_order_no(prefix: str = "REN") -> str:
    ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
    suffix = "".join(random.choices(string.digits, k=6))
    return f"{prefix}{ts}{suffix}"


def _cycle_to_days(cycle: str) -> int:
    mapping = {
        "hourly": 0,
        "daily": 1,
        "weekly": 7,
        "monthly": 30,
        "quarterly": 90,
        "semi_annually": 180,
        "semiannually": 180,
        "annually": 365,
        "yearly": 365,
        "biennially": 730,
        "triennially": 1095,
    }
    return mapping.get(cycle.lower(), 30)


def _cycle_to_timedelta(cycle: str) -> timedelta:
    if cycle.lower() == "hourly":
        return timedelta(hours=1)
    return timedelta(days=max(1, _cycle_to_days(cycle)))


def _auto_renew_candidate(instance: Instance, now: datetime) -> bool:
    expires_at = _aware(instance.expires_at)
    return bool(
        int(instance.auto_renew or 0) == 1
        and instance.status == InstanceStatus.ACTIVE
        and expires_at
        and expires_at > now
        and expires_at <= now + timedelta(days=AUTO_RENEW_BEFORE_DAYS)
    )


def _event_expiry_stamp(expires_at: datetime | None) -> str:
    if not expires_at:
        return "none"
    return expires_at.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S")


def _auto_renew_event_key(instance: Instance, old_expires_at: datetime, cycles: int) -> str:
    stamp = old_expires_at.astimezone(timezone.utc).strftime("%Y%m%d%H%M%S")
    return f"auto_renew:{instance.id}:{stamp}:{cycles}"


async def active_disk_addons(db: AsyncSession, instance_id: int, *, for_update: bool = False) -> list[InstanceDiskAddon]:
    stmt = (
        select(InstanceDiskAddon)
        .where(
            InstanceDiskAddon.instance_id == instance_id,
            InstanceDiskAddon.status == InstanceDiskAddonStatus.ACTIVE,
        )
        .order_by(InstanceDiskAddon.id.asc())
    )
    if for_update:
        stmt = stmt.with_for_update()
    try:
        res = await db.execute(stmt)
    except ProgrammingError as exc:
        if "instance_disk_addons" not in str(exc):
            raise
        await db.rollback()
        return []
    return list(res.scalars().all())


async def ensure_no_pending_disk_tasks(db: AsyncSession, instance_id: int, *, for_update: bool = False) -> None:
    stmt = select(InstanceDiskAddon.id).where(
        InstanceDiskAddon.instance_id == instance_id,
        InstanceDiskAddon.status == InstanceDiskAddonStatus.PENDING,
    )
    if for_update:
        stmt = stmt.with_for_update()
    try:
        res = await db.execute(stmt.limit(1))
    except ProgrammingError as exc:
        if "instance_disk_addons" not in str(exc):
            raise
        await db.rollback()
        return
    if res.scalar_one_or_none():
        raise PendingDiskTaskError()


async def _finalize_sales_rewards(
    db: AsyncSession,
    *,
    order: Order,
    user_id: int,
    paid_amount: int,
    instance_id: int,
    source_type: str,
) -> None:
    settlements = await agent_service.calculate_order_settlement(db, order, user_id, paid_amount)
    for settlement in settlements:
        db.add(settlement)
    await db.flush()

    if settlements:
        await agent_service.settle_order(db, order)

    await membership_service.reward_referrer_for_order(db, order)
    await membership_service.create_self_purchase_point_reward(
        db,
        user_id=user_id,
        order_id=order.id,
        ref_no=order.out_order_no,
        amount_cents=paid_amount,
        source_type=source_type,
        instance_id=instance_id,
        status="claimable",
        payload_snapshot={"instance_id": instance_id},
    )


async def renew_locked_instance(
    db: AsyncSession,
    instance: Instance,
    *,
    cycles: int = 1,
    source: str = "manual",
) -> RenewalResult:
    if instance.status not in (InstanceStatus.ACTIVE, InstanceStatus.SUSPENDED):
        raise RenewalError(400, "当前实例状态不支持续费")
    if cycles < 1 or cycles > 36:
        raise RenewalError(400, "续费周期数不合法")

    await ensure_no_pending_disk_tasks(db, instance.id, for_update=True)
    disk_addons = await active_disk_addons(db, instance.id, for_update=True)
    disk_addon_total = sum(int(addon.recurring_amount or 0) for addon in disk_addons)
    base_amount = _instance_base_renew_amount(instance)
    amount = base_amount + disk_addon_total
    if amount <= 0:
        raise RenewalError(400, "续费金额异常，请联系管理员")

    total_amount = amount * cycles
    user_res = await db.execute(select(User).where(User.id == instance.user_id).with_for_update())
    locked_user = user_res.scalar_one()
    if locked_user.balance < total_amount:
        raise InsufficientBalanceError(total_amount, int(locked_user.balance or 0))

    now = datetime.now(timezone.utc)
    base_expires_at = _aware(instance.expires_at) or now
    if base_expires_at < now:
        base_expires_at = now
    next_expires_at = base_expires_at + _cycle_to_timedelta(instance.cycle) * cycles
    old_expires_at_snapshot = instance.expires_at

    auto_renew_event: AutoRenewalEvent | None = None
    if source == "auto_renew":
        event_key = _auto_renew_event_key(instance, base_expires_at, cycles)
        auto_renew_event = AutoRenewalEvent(
            event_key=event_key,
            user_id=instance.user_id,
            instance_id=instance.id,
            status="processing",
            cycles=cycles,
            amount_cents=total_amount,
            old_expires_at=base_expires_at,
            new_expires_at=next_expires_at,
        )
        db.add(auto_renew_event)
        try:
            await db.flush()
        except IntegrityError as exc:
            raise DuplicateAutoRenewalError(event_key) from exc

    upstream_renewal: dict | None = None
    if await zjmf_instance_adapter.get_zjmf_server_for_instance(db, instance):
        if disk_addon_total > 0:
            raise RenewalError(400, zjmf_instance_adapter.UNSUPPORTED_OPERATION_MESSAGE)
        try:
            upstream_renewal = await zjmf_instance_adapter.renew_instance(db, instance, cycles=cycles)
        except HTTPException as exc:
            raise RenewalError(exc.status_code, str(exc.detail)) from exc
        except Exception as exc:  # noqa: BLE001
            log.exception("zjmf finance upstream renewal failed: instance=%s", instance.id)
            raise RenewalError(502, f"魔方财务续费失败：{exc}") from exc

        upstream_expires_at = _aware(instance.expires_at)
        if upstream_expires_at and upstream_expires_at > base_expires_at:
            next_expires_at = upstream_expires_at
            if auto_renew_event:
                auto_renew_event.new_expires_at = next_expires_at

    locked_user.balance -= total_amount
    out_order_no = _gen_renew_order_no("ARN" if source == "auto_renew" else "REN")
    source_label = "自动续费" if source == "auto_renew" else "续费"
    order = Order(
        user_id=instance.user_id,
        out_order_no=out_order_no,
        status=OrderStatus.PAID,
        total_amount=total_amount,
        discount_amount=0,
        items_snapshot=[{
            "type": "renewal",
            "source": source,
            "instance_id": instance.id,
            "product_id": instance.product_id,
            "cycle": instance.cycle,
            "cycles": cycles,
            "base_unit_price_cents": base_amount,
            "disk_addon_unit_price_cents": disk_addon_total,
            "disk_addons": [
                {
                    "id": addon.id,
                    "upstream_disk_id": addon.upstream_disk_id,
                    "size_gb": addon.size_gb,
                    "recurring_amount_cents": addon.recurring_amount,
                }
                for addon in disk_addons
            ],
            "unit_price_cents": amount,
            "item_total_cents": total_amount,
            "old_expires_at": old_expires_at_snapshot.isoformat() if old_expires_at_snapshot else None,
            "new_expires_at": next_expires_at.isoformat(),
            "upstream_renewal": upstream_renewal,
        }],
        remark=f"实例 #{instance.id} {source_label} {cycles} 个周期",
    )
    db.add(order)
    await db.flush()
    if auto_renew_event:
        auto_renew_event.order_id = order.id

    db.add(BalanceLog(
        user_id=instance.user_id,
        change_type=BalanceChangeType.ORDER_PAY,
        delta=-total_amount,
        balance_after=locked_user.balance,
        ref_no=out_order_no,
        remark=f"实例 #{instance.id} {source_label}",
    ))

    base_total = base_amount * cycles
    disk_total = disk_addon_total * cycles
    await spend_service.record_spend_event(
        db,
        user_id=instance.user_id,
        event_type=spend_service.SpendEventType.SERVER_RENEWAL,
        amount_cents=base_total,
        event_key=f"{out_order_no}:server_renewal",
        order_id=order.id,
        ref_no=out_order_no,
        remark=f"实例 #{instance.id} {source_label}基础费用",
        payload_snapshot={"instance_id": instance.id, "cycles": cycles, "source": source},
    )
    await spend_service.record_spend_event(
        db,
        user_id=instance.user_id,
        event_type=spend_service.SpendEventType.DISK_RENEWAL,
        amount_cents=disk_total,
        event_key=f"{out_order_no}:disk_renewal",
        order_id=order.id,
        ref_no=out_order_no,
        remark=f"实例 #{instance.id} {source_label}磁盘费用",
        payload_snapshot={
            "instance_id": instance.id,
            "cycles": cycles,
            "source": source,
            "disk_addons": [addon.id for addon in disk_addons],
        },
    )
    await _finalize_sales_rewards(
        db,
        order=order,
        user_id=instance.user_id,
        paid_amount=total_amount,
        instance_id=instance.id,
        source_type=spend_service.SpendEventType.SERVER_RENEWAL,
    )

    was_due_suspended = (
        instance.status == InstanceStatus.SUSPENDED
        and instance.suspend_type == SuspendType.DUE
    )

    instance.expires_at = next_expires_at
    instance.next_renew_amount = amount
    for addon in disk_addons:
        addon.charged_until = next_expires_at

    unsuspend_task = None
    if was_due_suspended:
        unsuspend_task = await instance_lifecycle_service.unsuspend_instance(
            db,
            instance,
            reason="续费后自动解除到期暂停",
            source="renewal",
            user_id=instance.user_id,
            order_id=order.id,
        )

    if auto_renew_event:
        auto_renew_event.status = "succeeded"
        auto_renew_event.processed_at = datetime.now(timezone.utc)

    await notification_service.safe_notify(
        db,
        user_id=instance.user_id,
        category="instance",
        level="success",
        title="自动续费成功" if source == "auto_renew" else "续费成功",
        content=(
            f"服务器 {_instance_name(instance)} {source_label}成功，"
            f"新到期时间 {_format_expiry_time(next_expires_at)}。"
        ),
        link=f"/control/server/{instance.id}",
        action_label="查看服务器",
        action_url=f"/control/server/{instance.id}",
    )

    await db.flush()
    return RenewalResult(
        order_id=order.id,
        order_no=out_order_no,
        amount=total_amount,
        balance=locked_user.balance,
        expires_at=next_expires_at,
        unsuspend_taskid=(unsuspend_task or {}).get("taskid") if unsuspend_task else None,
    )


async def _notify_issue_once(
    db: AsyncSession,
    *,
    event_key: str,
    event_type: str,
    user_id: int,
    instance_id: int,
    title: str,
    content: str,
    level: str = "warning",
    action_label: str | None = None,
    action_url: str | None = None,
) -> bool:
    existing = await db.execute(
        select(NotificationEvent.id).where(NotificationEvent.event_key == event_key).limit(1)
    )
    if existing.scalar_one_or_none():
        return False

    db.add(NotificationEvent(
        event_key=event_key,
        event_type=event_type,
        user_id=user_id,
        resource_type="instance",
        resource_id=instance_id,
    ))
    await db.flush()
    await notification_service.safe_notify(
        db,
        user_id=user_id,
        category="instance",
        level=level,
        title=title,
        content=content,
        link=action_url,
        action_label=action_label,
        action_url=action_url,
    )
    return True


async def _notify_auto_renew_insufficient(
    db: AsyncSession,
    *,
    user_id: int,
    instance_id: int,
    instance_name: str,
    expires_at: datetime | None,
    required_amount: int,
    current_balance: int,
) -> bool:
    stamp = _event_expiry_stamp(expires_at)
    return await _notify_issue_once(
        db,
        event_key=f"auto_renew_insufficient:{instance_id}:{stamp}",
        event_type="auto_renew_insufficient",
        user_id=user_id,
        instance_id=instance_id,
        title="自动续费失败：余额不足",
        content=(
            f"服务器 {instance_name} 将于 {_format_expiry_time(expires_at)} 到期，"
            f"自动续费需要 {_format_money(required_amount)}，当前余额 {_format_money(current_balance)}。"
            "充值后系统会继续尝试自动续费。"
        ),
        level="urgent",
        action_label="去充值",
        action_url="/finance",
    )


async def _notify_auto_renew_blocked(
    db: AsyncSession,
    *,
    user_id: int,
    instance_id: int,
    instance_name: str,
    expires_at: datetime | None,
    reason: str,
) -> bool:
    stamp = _event_expiry_stamp(expires_at)
    return await _notify_issue_once(
        db,
        event_key=f"auto_renew_blocked:{instance_id}:{stamp}:{reason[:40]}",
        event_type="auto_renew_blocked",
        user_id=user_id,
        instance_id=instance_id,
        title="自动续费暂未执行",
        content=(
            f"服务器 {instance_name} 将于 {_format_expiry_time(expires_at)} 到期，"
            f"因{reason}，暂未自动续费。"
        ),
        level="warning",
        action_label="查看续费",
        action_url=f"/control/server/{instance_id}?tab=renewal",
    )


async def process_auto_renewals(db: AsyncSession, *, limit: int = AUTO_RENEW_LIMIT) -> dict[str, int]:
    now = datetime.now(timezone.utc)
    result = await db.execute(
        select(Instance.id)
        .where(
            Instance.auto_renew == 1,
            Instance.status == InstanceStatus.ACTIVE,
            Instance.expires_at.is_not(None),
            Instance.expires_at > now,
            Instance.expires_at <= now + timedelta(days=AUTO_RENEW_BEFORE_DAYS),
        )
        .order_by(Instance.expires_at.asc(), Instance.id.asc())
        .limit(limit)
    )
    instance_ids = [int(instance_id) for instance_id in result.scalars().all()]

    renewed = 0
    deduped = 0
    insufficient = 0
    blocked = 0
    skipped = 0
    failed = 0

    for instance_id in instance_ids:
        try:
            inst_res = await db.execute(
                select(Instance).where(Instance.id == instance_id).with_for_update()
            )
            instance = inst_res.scalar_one_or_none()
            if not instance or not _auto_renew_candidate(instance, now):
                skipped += 1
                await db.rollback()
                continue

            context = {
                "user_id": instance.user_id,
                "instance_id": instance.id,
                "instance_name": _instance_name(instance),
                "expires_at": _aware(instance.expires_at),
            }
            result_item = await renew_locked_instance(
                db,
                instance,
                cycles=1,
                source="auto_renew",
            )
            renewed += 1
            await db.commit()
            log.info(
                "auto renewal succeeded: instance=%s order=%s amount=%s expires_at=%s",
                instance_id,
                result_item.order_no,
                result_item.amount,
                result_item.expires_at,
            )
        except DuplicateAutoRenewalError as exc:
            deduped += 1
            await db.rollback()
            log.info("auto renewal skipped by idempotency: instance=%s event_key=%s", instance_id, exc.event_key)
        except InsufficientBalanceError as exc:
            insufficient += 1
            await db.rollback()
            try:
                await _notify_auto_renew_insufficient(
                    db,
                    **context,
                    required_amount=exc.required_amount,
                    current_balance=exc.current_balance,
                )
                await db.commit()
            except Exception:
                await db.rollback()
                log.exception("auto renewal insufficient notification failed: instance=%s", instance_id)
        except PendingDiskTaskError:
            blocked += 1
            await db.rollback()
            try:
                await _notify_auto_renew_blocked(
                    db,
                    **context,
                    reason="存在未完成的磁盘任务",
                )
                await db.commit()
            except Exception:
                await db.rollback()
                log.exception("auto renewal blocked notification failed: instance=%s", instance_id)
        except RenewalError as exc:
            blocked += 1
            await db.rollback()
            try:
                await _notify_auto_renew_blocked(
                    db,
                    **context,
                    reason=exc.detail,
                )
                await db.commit()
            except Exception:
                await db.rollback()
                log.exception("auto renewal error notification failed: instance=%s", instance_id)
        except Exception:
            failed += 1
            await db.rollback()
            log.exception("auto renewal failed: instance=%s", instance_id)

    return {
        "renewed": renewed,
        "deduped": deduped,
        "insufficient": insufficient,
        "blocked": blocked,
        "skipped": skipped,
        "failed": failed,
    }
