from __future__ import annotations

import asyncio
import logging
import os
import socket
import uuid
from datetime import UTC, datetime, timedelta

from sqlalchemy import exists, or_, select
from sqlalchemy.orm import Session

from app.core.config import get_settings
from app.core.database import SessionLocal
from app.models.ai_config import SystemConfig
from app.models.growth import PeriodicReport, TopicSummary
from app.models.user import User
from app.services.entitlement_service import EntitlementService
from app.services.periodic_report_service import PeriodicReportService, calendar_period
from app.services.redis_client import get_sync_redis_client


logger = logging.getLogger(__name__)
WORKER_LOCK_KEY = "periodic-report:worker:lock"
WORKER_LOCK_TTL_SECONDS = 900
SCHEDULE_STATE_KEYS = {
    "weekly": "periodic_report_weekly_last_period_end",
    "monthly": "periodic_report_monthly_last_period_end",
}


class PeriodicReportWorker:
    """数据库持久化报告队列。

    Redis 只用于限制多进程并发；任务状态、重试和调度进度均以 MySQL 为准，
    因此 Redis 暂时不可用或服务重启时不会丢任务。
    """

    @classmethod
    async def run_forever(cls) -> None:
        settings = get_settings()
        if not settings.periodic_report_worker_enabled:
            logger.info("periodic report worker disabled")
            return
        worker_id = f"{socket.gethostname()}:{os.getpid()}:{uuid.uuid4().hex[:8]}"
        poll_seconds = max(1, settings.periodic_report_poll_seconds)
        while True:
            try:
                processed = await asyncio.to_thread(cls.run_once, worker_id)
            except Exception:
                processed = False
                logger.exception("periodic report worker iteration failed")
            await asyncio.sleep(0 if processed else poll_seconds)

    @classmethod
    def run_once(cls, worker_id: str) -> bool:
        redis, acquired = _acquire_worker_lock(worker_id)
        if not acquired:
            return False
        try:
            now = _now()
            with SessionLocal() as db:
                cls.recover_stale_jobs(db, now=now)
                cls.enqueue_due_schedules(db, now_utc=now)
                db.commit()
            with SessionLocal() as db:
                report_id = cls.claim_next(db, worker_id=worker_id, now=now)
            if report_id is None:
                return False
            with SessionLocal() as db:
                cls.execute_claimed(db, report_id=report_id, worker_id=worker_id)
            return True
        finally:
            _release_worker_lock(redis, worker_id)

    @staticmethod
    def claim_next(db: Session, *, worker_id: str, now: datetime | None = None) -> int | None:
        current = now or _now()
        report = db.scalar(
            select(PeriodicReport)
            .where(
                PeriodicReport.status == "pending",
                PeriodicReport.attempt_count < PeriodicReport.max_attempts,
                or_(PeriodicReport.next_run_at.is_(None), PeriodicReport.next_run_at <= current),
            )
            .order_by(PeriodicReport.next_run_at.asc(), PeriodicReport.id.asc())
            .with_for_update(skip_locked=True)
            .limit(1)
        )
        if report is None:
            db.rollback()
            return None
        report.status = "running"
        report.attempt_count += 1
        report.locked_at = current
        report.locked_by = worker_id
        report.last_started_at = current
        report.finished_at = None
        db.add(report)
        db.commit()
        return report.id

    @staticmethod
    def execute_claimed(db: Session, *, report_id: int, worker_id: str) -> PeriodicReport | None:
        report = db.get(PeriodicReport, report_id)
        if report is None or report.status != "running" or report.locked_by != worker_id:
            return report
        user = db.get(User, report.user_id)
        if user is None or user.is_deleted or user.status != 1:
            _finish_permanent_failure(report, "用户不存在或已停用")
            db.commit()
            return report
        if report.generated_by.startswith("schedule:"):
            entitlement = EntitlementService.active_entitlement(db, user)
            if not entitlement.enable_periodic_reports:
                _finish_permanent_failure(report, "用户当前权益未开启周期报告")
                db.commit()
                return report

        try:
            PeriodicReportService.generate_existing(db, report=report, user=user)
        except Exception as exc:
            report.status = "failed"
            report.error_message = str(exc)[:2000]

        now = _now()
        if report.status == "failed" and report.attempt_count < report.max_attempts:
            retry_seconds = min(300, 30 * (2 ** max(0, report.attempt_count - 1)))
            report.status = "pending"
            report.next_run_at = now + timedelta(seconds=retry_seconds)
            report.finished_at = None
        else:
            report.next_run_at = None
            report.finished_at = now
        report.locked_at = None
        report.locked_by = None
        db.add(report)
        db.commit()
        db.refresh(report)
        return report

    @staticmethod
    def recover_stale_jobs(db: Session, *, now: datetime | None = None) -> int:
        current = now or _now()
        stale_before = current - timedelta(minutes=max(5, get_settings().periodic_report_stale_minutes))
        reports = list(
            db.scalars(
                select(PeriodicReport)
                .where(
                    PeriodicReport.status == "running",
                    PeriodicReport.locked_at.is_not(None),
                    PeriodicReport.locked_at < stale_before,
                )
                .order_by(PeriodicReport.id.asc())
                .limit(100)
                .with_for_update(skip_locked=True)
            )
        )
        for report in reports:
            if report.attempt_count >= report.max_attempts:
                report.status = "failed"
                report.finished_at = current
                report.next_run_at = None
            else:
                report.status = "pending"
                report.next_run_at = current
                report.finished_at = None
            report.error_message = _append_error(report.error_message, "任务执行进程中断，系统已自动恢复")
            report.locked_at = None
            report.locked_by = None
            db.add(report)
        return len(reports)

    @staticmethod
    def enqueue_due_schedules(db: Session, *, now_utc: datetime | None = None) -> dict[str, int]:
        settings = get_settings()
        current = now_utc or _now()
        result: dict[str, int] = {}
        schedule_flags = {
            "weekly": settings.periodic_report_weekly_enabled,
            "monthly": settings.periodic_report_monthly_enabled,
        }
        for report_type, enabled in schedule_flags.items():
            if not enabled:
                continue
            period = scheduled_period(report_type, current, settings.periodic_report_timezone)
            if period is None:
                continue
            period_start, period_end = period
            state_key = SCHEDULE_STATE_KEYS[report_type]
            state = db.scalar(select(SystemConfig).where(SystemConfig.config_key == state_key))
            period_marker = period_end.isoformat()
            if state is not None and state.config_value == period_marker:
                continue
            enqueued = _enqueue_scheduled_users(
                db,
                report_type=report_type,
                period_start=period_start,
                period_end=period_end,
                now=current,
            )
            if state is None:
                state = SystemConfig(
                    config_key=state_key,
                    config_value=period_marker,
                    description=f"{report_type} 周期报告最近一次完成入队的周期结束时间",
                )
            else:
                state.config_value = period_marker
            db.add(state)
            result[report_type] = enqueued
        return result


def scheduled_period(
    report_type: str,
    now_utc: datetime,
    timezone_name: str = "Asia/Shanghai",
) -> tuple[datetime, datetime] | None:
    """返回最近一个已经到达生成时间的完整自然周或自然月。"""
    if report_type not in {"weekly", "monthly"}:
        return None
    return calendar_period(
        report_type,
        now_utc,
        timezone_name,
        respect_schedule_delay=True,
    )


def _enqueue_scheduled_users(
    db: Session,
    *,
    report_type: str,
    period_start: datetime,
    period_end: datetime,
    now: datetime,
) -> int:
    has_summary = exists(
        select(TopicSummary.id).where(
            TopicSummary.user_id == User.id,
            TopicSummary.status == "success",
            TopicSummary.generated_at >= period_start,
            TopicSummary.generated_at < period_end,
        )
    )
    users = list(
        db.scalars(
            select(User)
            .where(
                User.is_deleted == 0,
                User.status == 1,
                or_(User.effective_at.is_(None), User.effective_at <= now),
                or_(User.expired_at.is_(None), User.expired_at >= now),
                has_summary,
            )
            .order_by(User.id.asc())
        )
    )
    enqueued = 0
    for user in users:
        entitlement = EntitlementService.active_entitlement(db, user)
        if not entitlement.enable_periodic_reports:
            continue
        report = PeriodicReportService.enqueue_for_user(
            db,
            user=user,
            report_type=report_type,  # type: ignore[arg-type]
            period_start=period_start,
            period_end=period_end,
            generated_by=f"schedule:{report_type}",
            force=False,
        )
        if report.status == "pending":
            enqueued += 1
    return enqueued


def _finish_permanent_failure(report: PeriodicReport, message: str) -> None:
    report.status = "failed"
    report.error_message = message
    report.next_run_at = None
    report.locked_at = None
    report.locked_by = None
    report.finished_at = _now()


def _append_error(current: str | None, message: str) -> str:
    if not current:
        return message
    return f"{current}\n{message}"[-2000:]


def _acquire_worker_lock(worker_id: str):
    redis = get_sync_redis_client()
    if redis is None:
        return None, True
    try:
        return redis, bool(redis.set(WORKER_LOCK_KEY, worker_id, nx=True, ex=WORKER_LOCK_TTL_SECONDS))
    except Exception:
        logger.warning("redis unavailable for periodic report worker lock; falling back to database claim", exc_info=True)
        return None, True


def _release_worker_lock(redis, worker_id: str) -> None:
    if redis is None:
        return
    try:
        redis.eval(
            "if redis.call('get', KEYS[1]) == ARGV[1] then "
            "return redis.call('del', KEYS[1]) else return 0 end",
            1,
            WORKER_LOCK_KEY,
            worker_id,
        )
    except Exception:
        logger.warning("failed to release periodic report worker lock", exc_info=True)


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