# -*- coding: utf-8 -*-
# filename: file_preview.py
# @Time    : 2025-01-20
# @Author  : Claude Code
# @Email   : noreply@anthropic.com
# @Software: PyCharm
"""File preview tool for multiple file formats"""
import asyncio
import hashlib
import logging
import mimetypes
import os
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Any

import httpx

from tf_tools_mcp.tools.file_preview_tools.file_preview_constants import (
    get_file_type_from_extension,
)
from tf_tools_mcp.tools.file_preview_tools.file_preview_types import (
    ErrorCode,
    FileMetadata,
    FilePreviewOptions,
    FilePreviewParams,
    FilePreviewResult,
    FilePreviewStatus,
    SingleFileResult,
)
from tf_tools_mcp.tools.file_preview_tools.handlers import (
    DocumentHandler,
    ExcelHandler,
    ImageHandler,
    PDFHandler,
    PPTHandler,
)

logger = logging.getLogger(__name__)


class FilePreviewTool:
    """
    文件预览工具，支持多种文件格式的预览和内容描述生成

    支持的格式：
    - 图片：PNG, JPG, JPEG, WebP, GIF, BMP, TIFF, HEIC 等
    - PDF：.pdf
    - 文档：.docx, .doc, .txt, .md
    - 表格：.xlsx, .xls, .csv
    - 演示：.pptx, .ppt
    """

    name: str = "file_preview"
    description: str = "多格式文件预览工具，支持图片、PDF、Word、Excel、PPT 等格式，提取元数据并选择性生成内容描述"
    version: str = "0.1.0"
    tags: set[str] = {"file", "preview", "vision", "ocr", "document", "image"}

    def __init__(self, default_options: FilePreviewOptions | None = None, **data: Any):
        """初始化工具

        Args:
            default_options: 实例级别的默认配置，后续调用不传 options 时使用此配置
            **data: 其他属性
        """
        # 设置属性
        for key, value in data.items():
            setattr(self, key, value)

        # 设置默认配置（如果未提供，使用 FilePreviewOptions 的类默认值）
        self.default_options = default_options or FilePreviewOptions()

        # 初始化缓存
        self._cache: dict[str, dict] = {}

        # 处理器映射
        self._handlers = {
            "image": ImageHandler,
            "pdf": PDFHandler,
            "document": DocumentHandler,
            "excel": ExcelHandler,
            "ppt": PPTHandler,
        }

    async def _detect_file_type(self, file_path: str) -> str | None:
        """检测文件类型

        Args:
            file_path: 文件路径

        Returns:
            文件类型标识
        """
        path = Path(file_path)
        ext = path.suffix.lower()

        # 基于扩展名检测
        file_type = get_file_type_from_extension(ext)
        if file_type:
            return file_type

        # 尝试使用 MIME 类型检测
        mime_type, _ = mimetypes.guess_type(file_path)
        if mime_type:
            # 从 MIME 类型映射到文件类型
            if mime_type.startswith("image/"):
                return "image"
            elif mime_type == "application/pdf":
                return "pdf"
            elif mime_type in {
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                "application/msword",
                "text/plain",
                "text/markdown",
            }:
                return "document"
            elif mime_type in {
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                "application/vnd.ms-excel",
                "text/csv",
            }:
                return "excel"
            elif mime_type in {
                "application/vnd.openxmlformats-officedocument.presentationml.presentation",
                "application/vnd.ms-powerpoint",
            }:
                return "ppt"

        return None

    async def _download_file(self, url: str, options: FilePreviewOptions) -> str | None:
        """下载远程文件到临时目录

        Args:
            url: 文件 URL
            options: 预览选项

        Returns:
            临时文件路径，下载失败返回 None
        """
        try:
            async with httpx.AsyncClient(timeout=options.download_timeout) as client:
                response = await client.get(url)

                # 检查文件大小
                content_length = len(response.content)
                if content_length > options.max_download_size:
                    logger.error(f"Downloaded file too large: {content_length} bytes")
                    return None

                # 保存到临时文件
                temp_file = tempfile.NamedTemporaryFile(delete=False)
                temp_file.write(response.content)
                temp_file.close()

                return temp_file.name

        except Exception as e:
            logger.error(f"Failed to download file from {url}: {e}")
            return None

    def _get_cache_key(self, file_path: str, options: FilePreviewOptions) -> str:
        """生成缓存键

        Args:
            file_path: 文件路径
            options: 预览选项

        Returns:
            缓存键（SHA256 hash）
        """
        # 读取文件内容
        try:
            with open(file_path, "rb") as f:
                file_content = f.read()
        except Exception:
            # 如果读取失败，使用文件路径作为 fallback
            file_content = file_path.encode()

        # 包含所有参数
        options_str = str(options.model_dump())

        # 计算 SHA256 hash
        combined = file_content + options_str.encode()
        return hashlib.sha256(combined).hexdigest()

    async def _process_single_file(
        self,
        file_path: str,
        options: FilePreviewOptions,
        downloaded_temp_file: bool = False,
    ) -> SingleFileResult:
        """处理单个文件

        Args:
            file_path: 文件路径
            options: 预览选项
            downloaded_temp_file: 是否为下载的临时文件

        Returns:
            单文件处理结果
        """
        temp_file_to_cleanup = None

        try:
            # 检查是否为 URL
            is_url = file_path.startswith(("http://", "https://"))

            if is_url:
                # 下载文件
                temp_file = await self._download_file(file_path, options)
                if temp_file is None:
                    path = Path(file_path)
                    return SingleFileResult(
                        file_path=file_path,
                        status=FilePreviewStatus.FAILED,
                        error_code=ErrorCode.DOWNLOAD_FAILED,
                        error_message="Failed to download file",
                        metadata={
                            "filename": path.name,
                            "file_size": 0,
                            "file_type": "unknown",
                            "created_at": None,
                            "modified_at": None,
                        },
                        format_metadata={},
                    )
                temp_file_to_cleanup = temp_file
                actual_path = temp_file
            else:
                actual_path = file_path

            # 检查文件是否存在
            if not Path(actual_path).exists():
                return SingleFileResult(
                    file_path=file_path,
                    status=FilePreviewStatus.FAILED,
                    error_code=ErrorCode.FILE_NOT_FOUND,
                    error_message=f"File not found: {file_path}",
                    metadata={
                        "filename": Path(file_path).name,
                        "file_size": 0,
                        "file_type": "unknown",
                        "created_at": None,
                        "modified_at": None,
                    },
                    format_metadata={},
                )

            # 检查文件大小
            file_size = Path(actual_path).stat().st_size
            if file_size > options.max_file_size:
                return SingleFileResult(
                    file_path=file_path,
                    status=FilePreviewStatus.FAILED,
                    error_code=ErrorCode.FILE_TOO_LARGE,
                    error_message=f"File too large: {file_size} bytes (max: {options.max_file_size})",
                    metadata={
                        "filename": Path(file_path).name,
                        "file_size": file_size,
                        "file_type": "unknown",
                        "created_at": None,
                        "modified_at": None,
                    },
                    format_metadata={},
                )

            # 检测文件类型
            file_type = await self._detect_file_type(actual_path)
            if file_type is None:
                return SingleFileResult(
                    file_path=file_path,
                    status=FilePreviewStatus.FAILED,
                    error_code=ErrorCode.UNSUPPORTED_FORMAT,
                    error_message=f"Unsupported file format",
                    metadata={
                        "filename": Path(file_path).name,
                        "file_size": file_size,
                        "file_type": "unknown",
                        "created_at": None,
                        "modified_at": None,
                    },
                    format_metadata={},
                )

            # 获取对应的处理器
            handler_class = self._handlers.get(file_type)
            if handler_class is None:
                return SingleFileResult(
                    file_path=file_path,
                    status=FilePreviewStatus.FAILED,
                    error_code=ErrorCode.UNSUPPORTED_FORMAT,
                    error_message=f"No handler for file type: {file_type}",
                    metadata={
                        "filename": Path(file_path).name,
                        "file_size": file_size,
                        "file_type": "unknown",
                        "created_at": None,
                        "modified_at": None,
                    },
                    format_metadata={},
                )

            # 检查缓存
            cache_key = None
            if options.enable_cache:
                cache_key = self._get_cache_key(actual_path, options)
                if cache_key in self._cache:
                    cached_result = self._cache[cache_key]
                    logger.info(f"Cache hit for {file_path}")
                    return SingleFileResult(**cached_result)

            # 创建处理器实例
            handler = handler_class(options)

            # 处理文件
            result = await handler.process(actual_path)

            # 更新 file_path（如果是下载的临时文件，使用原始 URL）
            result.file_path = file_path

            # 缓存结果
            if options.enable_cache and cache_key:
                self._cache[cache_key] = result.model_dump()

            return result

        finally:
            # 清理临时文件
            if temp_file_to_cleanup:
                try:
                    os.unlink(temp_file_to_cleanup)
                except Exception as e:
                    logger.warning(
                        f"Failed to cleanup temp file {temp_file_to_cleanup}: {e}"
                    )

    async def run(self, params: FilePreviewParams) -> FilePreviewResult:
        """
        运行文件预览工具

        Args:
            params: 文件预览参数（如果不提供 options，使用实例化时的默认配置）

        Returns:
            FilePreviewResult 批量处理结果
        """
        # 优先使用本次调用的 options，否则使用实例级别的默认配置
        options = params.options or self.default_options

        # 确定并发数
        max_workers = options.max_workers if options.max_workers is not None else 4

        # 创建信号量控制并发
        semaphore = asyncio.Semaphore(max_workers)

        async def process_with_semaphore(file_path: str) -> SingleFileResult:
            async with semaphore:
                return await self._process_single_file(file_path, options)

        # 并发处理所有文件（去除文件路径两侧空格）
        tasks = [process_with_semaphore(fp.strip()) for fp in params.file_paths]
        results = await asyncio.gather(*tasks)

        # 统计结果
        total_files = len(results)
        success_count = sum(1 for r in results if r.status == FilePreviewStatus.SUCCESS)
        failed_count = sum(1 for r in results if r.status == FilePreviewStatus.FAILED)

        return FilePreviewResult(
            results=list(results),
            total_files=total_files,
            success_count=success_count,
            failed_count=failed_count,
            processed_at=datetime.now().isoformat(),
        )

    @staticmethod
    def _format_metadata(metadata: FileMetadata, format_metadata: dict) -> str:
        """格式化元数据为可读文本

        Args:
            metadata: 基础元数据
            format_metadata: 格式特定的元数据

        Returns:
            格式化的元数据文本
        """
        from .handlers.base import format_file_size

        lines = []

        # 基础信息
        lines.append(f"文件名：{metadata.filename}")
        lines.append(f"大小：{format_file_size(metadata.file_size)}")
        lines.append(f"类型：{metadata.file_type}")

        # 根据文件类型添加特定信息
        if metadata.file_type == "image":
            if "width" in format_metadata and "height" in format_metadata:
                lines.append(
                    f"尺寸：{format_metadata['width']}x{format_metadata['height']}"
                )
            if "format" in format_metadata:
                lines.append(f"格式：{format_metadata['format']}")
        elif metadata.file_type == "pdf":
            if "page_count" in format_metadata:
                lines.append(f"页数：{format_metadata['page_count']}")
        elif metadata.file_type == "ppt":
            if "slide_count" in format_metadata:
                lines.append(f"页数：{format_metadata['slide_count']}")
        elif metadata.file_type == "excel":
            if "sheet_count" in format_metadata:
                lines.append(f"工作表数：{format_metadata['sheet_count']}")
        elif metadata.file_type == "document":
            if "estimated_pages" in format_metadata:
                lines.append(f"预估页数：{format_metadata['estimated_pages']}")
            if "char_count" in format_metadata:
                lines.append(f"字符数：{format_metadata['char_count']}")

        # 添加时间信息（如果有）
        if metadata.created_at:
            lines.append(f"创建时间：{metadata.created_at}")
        if metadata.modified_at:
            lines.append(f"修改时间：{metadata.modified_at}")

        return "\n".join(lines)

    @staticmethod
    def format_output(result: FilePreviewResult) -> str:
        """格式化结果供 LLM 消费"""
        output = []
        for r in result.results:
            if r.status == FilePreviewStatus.SUCCESS:
                # 格式化元数据
                metadata_text = FilePreviewTool._format_metadata(
                    r.metadata, r.format_metadata
                )
                output.append(metadata_text)

                # 添加内容描述（如果有）
                if r.content_summary:
                    output.append(f"\n内容描述：\n{r.content_summary}")
            else:
                output.append(f"文件: {r.file_path} - 失败: {r.error_message}")

        return "\n\n".join(output)
