"""商业自动续期后台调度；PostgreSQL advisory lock 选举单一执行者。"""

from __future__ import annotations

import os
import threading
from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select, text
from sqlalchemy.orm import Session

from app.core.logging import get_logger
from app.db.session import get_session_factory
from app.models.commercial import TenantSubscription
from app.services.commercial_subscription_rollover import (
    CommercialSubscriptionRolloverService,
)

logger = get_logger("app.services.commercial_rollover_scheduler")
_LEASE_KEY = "x-financial:commercial-rollover-scheduler:v1"


class CommercialRolloverScheduler:
    def __init__(
        self,
        *,
        session_factory: Callable[[], Session] | None = None,
    ) -> None:
        self._interval_seconds = max(
            30,
            _env_int("X_FINANCIAL_COMMERCIAL_ROLLOVER_INTERVAL_SECONDS", 300),
        )
        self._initial_delay_seconds = max(
            1,
            _env_int("X_FINANCIAL_COMMERCIAL_ROLLOVER_INITIAL_DELAY_SECONDS", 20),
        )
        self._batch_size = min(
            500,
            max(1, _env_int("X_FINANCIAL_COMMERCIAL_ROLLOVER_BATCH_SIZE", 100)),
        )
        self._session_factory = session_factory
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None
        self._lock = threading.Lock()

    def start(self) -> None:
        with self._lock:
            if self._thread is not None and self._thread.is_alive():
                return
            self._stop_event.clear()
            self._thread = threading.Thread(
                target=self._run_loop,
                name="commercial-rollover-scheduler",
                daemon=True,
            )
            self._thread.start()
        logger.info("Commercial rollover scheduler started interval=%ss", self._interval_seconds)

    def shutdown(self) -> None:
        with self._lock:
            thread = self._thread
            self._thread = None
        self._stop_event.set()
        if thread is not None and thread.is_alive():
            thread.join(timeout=3)
        logger.info("Commercial rollover scheduler stopped")

    def _run_loop(self) -> None:
        if self._stop_event.wait(self._initial_delay_seconds):
            return
        while not self._stop_event.is_set():
            try:
                self._run_once()
            except Exception:  # pragma: no cover - 后台循环最后防线
                logger.exception("Scheduled commercial subscription rollover failed")
            if self._stop_event.wait(self._interval_seconds):
                break

    def _run_once(self, *, as_of: datetime | None = None) -> dict[str, Any]:
        when = _utc(as_of or datetime.now(UTC))
        factory = self._session_factory or get_session_factory()
        db = factory()
        lease_acquired = False
        summary: dict[str, Any] = {
            "scanned": 0,
            "rolled_over": 0,
            "periods_created": 0,
            "ineligible": 0,
            "errors": [],
            "leader_skipped": 0,
        }
        try:
            lease_acquired = self._try_acquire_lease(db)
            if not lease_acquired:
                summary["leader_skipped"] = 1
                return summary
            candidates = list(
                db.execute(
                    select(TenantSubscription.tenant_id, TenantSubscription.id)
                    .where(
                        TenantSubscription.status.in_(("trialing", "active")),
                        TenantSubscription.auto_renew.is_(True),
                        TenantSubscription.current_period_end <= when,
                    )
                    .order_by(
                        TenantSubscription.current_period_end,
                        TenantSubscription.id,
                    )
                    .limit(self._batch_size)
                ).all()
            )
            summary["scanned"] = len(candidates)
            for tenant_id, subscription_id in candidates:
                try:
                    result = CommercialSubscriptionRolloverService(db).rollover_due(
                        tenant_id,
                        subscription_id,
                        as_of=when,
                    )
                    db.commit()
                    if result.status in {"rolled_over", "replayed"}:
                        summary["rolled_over"] += 1
                        summary["periods_created"] += len(result.created_period_ids)
                    elif result.status == "ineligible":
                        summary["ineligible"] += 1
                except Exception as error:  # 单租户失败不阻断其他租户
                    db.rollback()
                    summary["errors"].append(
                        {"subscription_id": subscription_id, "error": str(error)}
                    )
                    logger.exception(
                        "Commercial rollover failed subscription_id=%s",
                        subscription_id,
                    )
            if summary["rolled_over"] or summary["errors"]:
                logger.info("Commercial rollover cycle summary=%s", summary)
            return summary
        finally:
            if lease_acquired:
                try:
                    self._release_lease(db)
                except Exception:  # pragma: no cover - 连接故障兜底
                    db.rollback()
                    logger.exception("Failed to release commercial rollover lease")
            db.close()

    @staticmethod
    def _try_acquire_lease(db: Session) -> bool:
        bind = db.get_bind()
        if bind is None or bind.dialect.name != "postgresql":
            return True
        return bool(
            db.scalar(
                text("SELECT pg_try_advisory_lock(hashtextextended(:lease_key, 0))"),
                {"lease_key": _LEASE_KEY},
            )
        )

    @staticmethod
    def _release_lease(db: Session) -> None:
        bind = db.get_bind()
        if bind is None or bind.dialect.name != "postgresql":
            return
        released = db.scalar(
            text("SELECT pg_advisory_unlock(hashtextextended(:lease_key, 0))"),
            {"lease_key": _LEASE_KEY},
        )
        if not released:
            logger.warning("Commercial rollover scheduler lease was not owned")


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


def _env_int(name: str, default: int) -> int:
    try:
        return int(str(os.environ.get(name) or default).strip())
    except (TypeError, ValueError, OverflowError):
        return default


commercial_rollover_scheduler = CommercialRolloverScheduler()
