"""租户级 Agent 资产真实发布遥测周期监控。"""

from __future__ import annotations

import os
import threading
from collections.abc import Callable
from typing import Any

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

from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentAssetType
from app.core.logging import get_logger
from app.db.session import get_session_factory
from app.models.agent_asset import AgentAsset
from app.services.agent_asset_release_monitor import AgentAssetReleaseMonitor

logger = get_logger("app.services.agent_asset_release_scheduler")

_MONITORED_STAGES = {"shadow", "canary", "active"}
_SCHEDULER_LEASE_KEY = "x-financial:agent-asset-release-scheduler:v1"


class AgentAssetReleaseScheduler:
    def __init__(
        self,
        *,
        session_factory: Callable[[], Session] | None = None,
    ) -> None:
        self._interval_seconds = max(
            30,
            _env_int("X_FINANCIAL_RELEASE_MONITOR_INTERVAL_SECONDS", 60),
        )
        self._initial_delay_seconds = max(
            1,
            _env_int("X_FINANCIAL_RELEASE_MONITOR_INITIAL_DELAY_SECONDS", 15),
        )
        self._batch_size = min(
            500,
            max(1, _env_int("X_FINANCIAL_RELEASE_MONITOR_BATCH_SIZE", 100)),
        )
        self._session_factory = session_factory
        self._tenant_cursors: dict[str, str] = {}
        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="agent-asset-release-scheduler",
                daemon=True,
            )
            self._thread.start()
        logger.info(
            "Agent asset release scheduler started interval=%ss batch=%s",
            self._interval_seconds,
            self._batch_size,
        )

    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("Agent asset release 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 Agent asset release monitoring failed")
            if self._stop_event.wait(self._interval_seconds):
                break

    def _run_once(self) -> dict[str, Any]:
        factory = self._session_factory or get_session_factory()
        db = factory()
        lease_acquired = False
        try:
            lease_acquired = self._try_acquire_lease(db)
            if not lease_acquired:
                logger.info("Agent asset release monitor cycle skipped: lease held by peer")
                return {
                    "tenants": 0,
                    "scanned": 0,
                    "evaluated": 0,
                    "collecting": 0,
                    "rolled_back": 0,
                    "errors": 0,
                    "global_skipped": 0,
                    "leader_skipped": 1,
                }
            tenants, global_count = self._tenant_targets(db)
            summary: dict[str, Any] = {
                "tenants": len(tenants),
                "scanned": 0,
                "evaluated": 0,
                "collecting": 0,
                "rolled_back": 0,
                "errors": 0,
                "global_skipped": global_count,
            }
            monitor = AgentAssetReleaseMonitor(db)
            for tenant_id in tenants:
                result = monitor.batch_evaluate(
                    tenant_id=tenant_id,
                    actor="release-telemetry-scheduler",
                    limit=self._batch_size,
                    after_asset_id=self._tenant_cursors.get(tenant_id),
                )
                next_cursor = str(result.get("next_cursor") or "").strip()
                if next_cursor:
                    self._tenant_cursors[tenant_id] = next_cursor
                for key in ("scanned", "evaluated", "collecting", "rolled_back"):
                    summary[key] += int(result[key])
                summary["errors"] += len(result["errors"])
            if any(summary[key] for key in ("evaluated", "rolled_back", "errors")):
                logger.info("Agent asset release monitor cycle summary=%s", summary)
            if global_count:
                logger.warning(
                    "Global release assets skipped by tenant scheduler count=%s",
                    global_count,
                )
            return summary
        except Exception:
            db.rollback()
            raise
        finally:
            if lease_acquired:
                try:
                    self._release_lease(db)
                except Exception:  # pragma: no cover - 数据库连接故障兜底
                    db.rollback()
                    logger.exception("Failed to release Agent asset scheduler 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": _SCHEDULER_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": _SCHEDULER_LEASE_KEY},
        )
        if not released:
            logger.warning("Agent asset release scheduler lease was not owned at release time")

    def _tenant_targets(self, db: Session) -> tuple[list[str], int]:
        rows = list(
            db.execute(
                select(AgentAsset.tenant_id, AgentAsset.scope, AgentAsset.config_json)
                .where(
                    AgentAsset.asset_type == AgentAssetType.RULE.value,
                    AgentAsset.domain == AgentAssetDomain.EXPENSE.value,
                    AgentAsset.status != AgentAssetStatus.DISABLED.value,
                    AgentAsset.config_json["detail_mode"].as_string() == "json_risk",
                    AgentAsset.config_json["release_guard"]["stage"]
                    .as_string()
                    .in_(sorted(_MONITORED_STAGES)),
                )
            ).all()
        )
        tenants: set[str] = set()
        global_count = 0
        for asset_tenant_id, asset_scope, value in rows:
            config = value if isinstance(value, dict) else {}
            state = config.get("release_guard")
            if (
                str(config.get("detail_mode") or "").strip().lower() != "json_risk"
                or config.get("enabled") is False
                or not isinstance(state, dict)
                or str(state.get("stage") or "").strip().lower() not in _MONITORED_STAGES
            ):
                continue
            if asset_scope == "tenant" and asset_tenant_id != "platform":
                tenants.add(str(asset_tenant_id))
            elif asset_scope == "platform" and asset_tenant_id == "platform":
                global_count += 1
        return sorted(tenants), global_count


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


agent_asset_release_scheduler = AgentAssetReleaseScheduler()
