#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：cleanup_service.py
模块描述：数据清理服务，负责清理测试执行产生的临时文件和历史数据

功能：
1. 清理过期截图文件
2. 清理过期报告文件
3. 清理过期执行记录
4. 归档日志文件
5. 数据库优化

作者：czj
创建日期：2026-07-12
"""

import os
import logging
import shutil
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, List, Dict, Any

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete, func

from app.models.execution import Execution
from app.models.case_result import CaseResult
from app.config import settings

logger = logging.getLogger(__name__)


class CleanupService:
    """
    数据清理服务

    负责清理测试执行产生的临时文件和历史数据，包括：
    - 过期截图文件
    - 过期报告文件
    - 过期执行记录
    - 数据库优化

    Attributes:
        db (AsyncSession): 数据库会话
        screenshot_dir (Path): 截图目录
        report_dir (Path): 报告目录

    Example:
        >>> service = CleanupService(db)
        >>> result = await service.cleanup_all(days=30)
        >>> print(result)
        {'screenshots': 50, 'reports': 10, 'executions': 5}
    """

    def __init__(
        self,
        db: AsyncSession,
        screenshot_dir: Optional[str] = None,
        report_dir: Optional[str] = None,
    ):
        """
        初始化清理服务

        Args:
            db (AsyncSession): 数据库会话
            screenshot_dir (Optional[str]): 截图目录
            report_dir (Optional[str]): 报告目录
        """
        self.db = db
        self.screenshot_dir = Path(screenshot_dir or settings.SCREENSHOT_DIR)
        self.report_dir = Path(report_dir or settings.REPORT_DIR)

        # 确保目录存在
        self.screenshot_dir.mkdir(parents=True, exist_ok=True)
        self.report_dir.mkdir(parents=True, exist_ok=True)

    async def cleanup_all(
        self,
        days: int = 30,
        dry_run: bool = False,
    ) -> Dict[str, Any]:
        """
        执行完整清理

        Args:
            days (int): 保留天数，超过此天数的数据将被清理
            dry_run (bool): 是否仅模拟运行（不实际删除）

        Returns:
            Dict[str, Any]: 清理结果统计
        """
        result = {
            "screenshots": 0,
            "reports": 0,
            "executions": 0,
            "case_results": 0,
            "freed_space": 0,
            "dry_run": dry_run,
        }

        logger.info(f"开始清理数据，保留天数: {days}, 模拟运行: {dry_run}")

        # 1. 清理截图
        screenshots_result = await self.cleanup_screenshots(days=days, dry_run=dry_run)
        result["screenshots"] = screenshots_result["count"]
        result["freed_space"] += screenshots_result["size"]

        # 2. 清理报告
        reports_result = await self.cleanup_reports(days=days, dry_run=dry_run)
        result["reports"] = reports_result["count"]
        result["freed_space"] += reports_result["size"]

        # 3. 清理执行记录
        executions_result = await self.cleanup_executions(days=days, dry_run=dry_run)
        result["executions"] = executions_result["executions"]
        result["case_results"] = executions_result["case_results"]

        # 4. 数据库优化（仅非模拟运行时执行）
        if not dry_run:
            await self.optimize_database()

        logger.info(
            f"清理完成: 截图={result['screenshots']}, 报告={result['reports']}, "
            f"执行记录={result['executions']}, 释放空间={result['freed_space'] / 1024 / 1024:.2f}MB"
        )

        return result

    async def cleanup_screenshots(
        self,
        days: int = 30,
        dry_run: bool = False,
    ) -> Dict[str, Any]:
        """
        清理过期截图文件

        Args:
            days (int): 保留天数
            dry_run (bool): 是否模拟运行

        Returns:
            Dict[str, Any]: 清理结果
        """
        cutoff_date = datetime.now() - timedelta(days=days)
        deleted_count = 0
        deleted_size = 0

        if not self.screenshot_dir.exists():
            return {"count": 0, "size": 0}

        for file_path in self.screenshot_dir.iterdir():
            if not file_path.is_file():
                continue

            # 检查文件修改时间
            mtime = datetime.fromtimestamp(file_path.stat().st_mtime)
            if mtime < cutoff_date:
                file_size = file_path.stat().st_size

                if not dry_run:
                    try:
                        file_path.unlink()
                        logger.debug(f"删除截图: {file_path.name}")
                    except Exception as e:
                        logger.error(f"删除截图失败 {file_path.name}: {str(e)}")
                        continue

                deleted_count += 1
                deleted_size += file_size

        logger.info(f"截图清理: 删除 {deleted_count} 个文件, 释放 {deleted_size / 1024 / 1024:.2f}MB")
        return {"count": deleted_count, "size": deleted_size}

    async def cleanup_reports(
        self,
        days: int = 30,
        dry_run: bool = False,
    ) -> Dict[str, Any]:
        """
        清理过期报告文件

        Args:
            days (int): 保留天数
            dry_run (bool): 是否模拟运行

        Returns:
            Dict[str, Any]: 清理结果
        """
        cutoff_date = datetime.now() - timedelta(days=days)
        deleted_count = 0
        deleted_size = 0

        if not self.report_dir.exists():
            return {"count": 0, "size": 0}

        for file_path in self.report_dir.iterdir():
            if not file_path.is_file():
                continue

            # 检查文件修改时间
            mtime = datetime.fromtimestamp(file_path.stat().st_mtime)
            if mtime < cutoff_date:
                file_size = file_path.stat().st_size

                if not dry_run:
                    try:
                        file_path.unlink()
                        logger.debug(f"删除报告: {file_path.name}")
                    except Exception as e:
                        logger.error(f"删除报告失败 {file_path.name}: {str(e)}")
                        continue

                deleted_count += 1
                deleted_size += file_size

        logger.info(f"报告清理: 删除 {deleted_count} 个文件, 释放 {deleted_size / 1024 / 1024:.2f}MB")
        return {"count": deleted_count, "size": deleted_size}

    async def cleanup_executions(
        self,
        days: int = 30,
        dry_run: bool = False,
    ) -> Dict[str, int]:
        """
        清理过期执行记录

        Args:
            days (int): 保留天数
            dry_run (bool): 是否模拟运行

        Returns:
            Dict[str, int]: 清理结果
        """
        cutoff_date = datetime.now() - timedelta(days=days)

        # 查询过期的执行记录
        query = select(Execution).where(
            Execution.created_at < cutoff_date,
            Execution.status.in_(["completed", "cancelled", "failed"]),
        )
        result = await self.db.execute(query)
        old_executions = list(result.scalars().all())

        execution_count = len(old_executions)
        case_result_count = 0

        if not dry_run and old_executions:
            # 先删除关联的 case_results
            execution_ids = [e.id for e in old_executions]

            # 统计 case_results 数量
            count_query = select(func.count(CaseResult.id)).where(
                CaseResult.execution_id.in_(execution_ids)
            )
            count_result = await self.db.execute(count_query)
            case_result_count = count_result.scalar() or 0

            # 删除 case_results
            delete_results = delete(CaseResult).where(
                CaseResult.execution_id.in_(execution_ids)
            )
            await self.db.execute(delete_results)

            # 删除 executions
            delete_executions = delete(Execution).where(
                Execution.id.in_(execution_ids)
            )
            await self.db.execute(delete_executions)

            await self.db.commit()

        logger.info(
            f"执行记录清理: 删除 {execution_count} 条执行记录, "
            f"{case_result_count} 条用例结果"
        )

        return {
            "executions": execution_count,
            "case_results": case_result_count,
        }

    async def cleanup_orphan_screenshots(self, dry_run: bool = False) -> Dict[str, Any]:
        """
        清理孤儿截图（没有关联到任何执行记录的截图）

        Args:
            dry_run (bool): 是否模拟运行

        Returns:
            Dict[str, Any]: 清理结果
        """
        # 获取数据库中所有截图路径
        query = select(CaseResult.steps_result)
        result = await self.db.execute(query)
        all_steps = result.scalars().all()

        # 提取所有截图路径
        referenced_screenshots = set()
        for steps in all_steps:
            if not steps:
                continue
            for step in steps:
                screenshot = step.get("screenshot")
                if screenshot:
                    # 提取文件名
                    referenced_screenshots.add(Path(screenshot).name)

        # 检查截图目录中的文件
        deleted_count = 0
        deleted_size = 0

        if not self.screenshot_dir.exists():
            return {"count": 0, "size": 0}

        for file_path in self.screenshot_dir.iterdir():
            if not file_path.is_file():
                continue

            if file_path.name not in referenced_screenshots:
                file_size = file_path.stat().st_size

                if not dry_run:
                    try:
                        file_path.unlink()
                        logger.debug(f"删除孤儿截图: {file_path.name}")
                    except Exception as e:
                        logger.error(f"删除孤儿截图失败 {file_path.name}: {str(e)}")
                        continue

                deleted_count += 1
                deleted_size += file_size

        logger.info(f"孤儿截图清理: 删除 {deleted_count} 个文件")
        return {"count": deleted_count, "size": deleted_size}

    async def optimize_database(self) -> None:
        """
        优化数据库（执行 VACUUM 操作）

        仅适用于 SQLite 数据库。
        """
        if "sqlite" not in settings.DATABASE_URL.lower():
            logger.info("非 SQLite 数据库，跳过 VACUUM")
            return

        try:
            # SQLite VACUUM 需要在事务外执行
            await self.db.execute("VACUUM")
            logger.info("数据库优化完成 (VACUUM)")
        except Exception as e:
            logger.error(f"数据库优化失败: {str(e)}")

    async def get_storage_stats(self) -> Dict[str, Any]:
        """
        获取存储统计信息

        Returns:
            Dict[str, Any]: 存储统计
        """
        stats = {
            "screenshots": {
                "count": 0,
                "size": 0,
                "oldest": None,
                "newest": None,
            },
            "reports": {
                "count": 0,
                "size": 0,
                "oldest": None,
                "newest": None,
            },
            "database": {
                "size": 0,
                "executions": 0,
                "case_results": 0,
            },
        }

        # 统计截图
        if self.screenshot_dir.exists():
            files = list(self.screenshot_dir.iterdir())
            stats["screenshots"]["count"] = len([f for f in files if f.is_file()])
            stats["screenshots"]["size"] = sum(
                f.stat().st_size for f in files if f.is_file()
            )
            if files:
                mtimes = [datetime.fromtimestamp(f.stat().st_mtime) for f in files if f.is_file()]
                if mtimes:
                    stats["screenshots"]["oldest"] = min(mtimes).isoformat()
                    stats["screenshots"]["newest"] = max(mtimes).isoformat()

        # 统计报告
        if self.report_dir.exists():
            files = list(self.report_dir.iterdir())
            stats["reports"]["count"] = len([f for f in files if f.is_file()])
            stats["reports"]["size"] = sum(
                f.stat().st_size for f in files if f.is_file()
            )
            if files:
                mtimes = [datetime.fromtimestamp(f.stat().st_mtime) for f in files if f.is_file()]
                if mtimes:
                    stats["reports"]["oldest"] = min(mtimes).isoformat()
                    stats["reports"]["newest"] = max(mtimes).isoformat()

        # 统计数据库
        db_path = Path(settings.DATABASE_URL.replace("sqlite+aiosqlite:///", ""))
        if db_path.exists():
            stats["database"]["size"] = db_path.stat().st_size

        # 统计记录数
        exec_count_query = select(func.count(Execution.id))
        exec_count_result = await self.db.execute(exec_count_query)
        stats["database"]["executions"] = exec_count_result.scalar() or 0

        result_count_query = select(func.count(CaseResult.id))
        result_count_result = await self.db.execute(result_count_query)
        stats["database"]["case_results"] = result_count_result.scalar() or 0

        return stats
