"""
Filesystem MCP Tool Provider - Phase 13.2 #82
==============================================

提供安全受限的文件讀取工具：
- read_file: 讀取文件內容
- list_directory: 列出目錄
- search_in_file: 搜尋文件內容

安全機制:
1. 目錄白名單 (只允許 /var/log, /tmp/awoooi 等)
2. 敏感文件黑名單 (.env, credentials, secrets)
3. 檔案大小限制 (預設 1MB)
4. 路徑遍歷防護 (禁止 ../)
5. Symlink 防護 (解析後必須在白名單內)

透過 DI 注入配置，不直接讀取環境變數。

@see docs/adr/ADR-015-mcp-modular-architecture.md
@author Claude Code
@version 1.0
@created 2026-03-26 (台北時區)
"""

import os
import re
import uuid
from pathlib import Path
from typing import Any

import structlog

from src.plugins.mcp.interfaces import MCPTool, MCPToolProvider, MCPToolResult

logger = structlog.get_logger(__name__)

# =============================================================================
# Security Configuration (可從 config.py 覆蓋)
# =============================================================================

# 目錄白名單 - 只允許讀取這些目錄
# 注意: macOS 上 /var 是 symlink 到 /private/var，需同時包含兩者
DEFAULT_ALLOWED_DIRECTORIES: list[str] = [
    "/var/log",           # 系統日誌 (Linux)
    "/private/var/log",   # 系統日誌 (macOS symlink 解析後)
    "/tmp/awoooi",        # AWOOOI 暫存目錄
    "/private/tmp/awoooi",  # macOS 暫存
    "/var/log/awoooi",    # AWOOOI 日誌
    "/var/log/nginx",     # Nginx 日誌
    "/var/log/kubernetes",  # K8s 日誌
    "/var/log/containers",  # K8s 容器日誌
    "/var/log/pods",      # K8s Pod 日誌
]

# 敏感文件黑名單 - 禁止讀取這些 pattern
SENSITIVE_FILE_PATTERNS: list[str] = [
    r"\.env$",            # 環境變數
    r"\.env\..+$",        # .env.local, .env.production
    r"credentials?\..*$", # credentials.json, credential.yaml
    r"secrets?\..*$",     # secret.yaml, secrets.json
    r"\.pem$",            # 憑證
    r"\.key$",            # 私鑰
    r"\.p12$",            # PKCS12
    r"\.pfx$",            # 憑證
    r"id_rsa.*$",         # SSH 私鑰
    r"\.ssh/.*$",         # SSH 目錄
    r"password.*$",       # 密碼檔案
    r"token.*$",          # Token 檔案
    r"kubeconfig.*$",     # K8s 配置
    r"\.kube/.*$",        # K8s 目錄
    r"shadow$",           # /etc/shadow
    r"passwd$",           # /etc/passwd (雖然不敏感但通常不需要)
]

# 檔案大小限制 (bytes)
DEFAULT_MAX_FILE_SIZE: int = 1 * 1024 * 1024  # 1MB

# 搜尋結果行數限制
DEFAULT_MAX_SEARCH_RESULTS: int = 100


class FilesystemSecurityError(Exception):
    """檔案系統安全錯誤"""
    pass


class FilesystemProvider(MCPToolProvider):
    """
    Filesystem MCP Tool Provider

    提供安全受限的文件讀取功能，所有操作都經過：
    1. 白名單驗證 (目錄)
    2. 黑名單驗證 (敏感文件)
    3. 路徑正規化 (防止遍歷攻擊)
    4. 大小限制 (防止 OOM)

    Security Notes:
    - 此 Provider 只提供讀取功能，不提供寫入
    - 所有路徑在使用前都會被 resolve() 成絕對路徑
    - Symlink 會被解析，解析後的路徑必須仍在白名單內
    """

    def __init__(
        self,
        allowed_directories: list[str] | None = None,
        max_file_size: int = DEFAULT_MAX_FILE_SIZE,
        max_search_results: int = DEFAULT_MAX_SEARCH_RESULTS,
    ) -> None:
        """
        初始化 Provider

        Args:
            allowed_directories: 允許讀取的目錄白名單
            max_file_size: 最大檔案大小 (bytes)
            max_search_results: 搜尋結果最大行數
        """
        self._allowed_directories = allowed_directories or DEFAULT_ALLOWED_DIRECTORIES
        self._max_file_size = max_file_size
        self._max_search_results = max_search_results

        # 預編譯敏感文件 pattern
        self._sensitive_patterns = [
            re.compile(pattern, re.IGNORECASE)
            for pattern in SENSITIVE_FILE_PATTERNS
        ]

        logger.info(
            "filesystem_provider_initialized",
            allowed_directories=self._allowed_directories,
            max_file_size=self._max_file_size,
        )

    @property
    def name(self) -> str:
        return "filesystem"

    async def list_tools(self) -> list[MCPTool]:
        return [
            MCPTool(
                name="read_file",
                description="Read file contents (max 1MB, restricted to whitelisted directories)",
                input_schema={
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Absolute file path",
                        },
                        "encoding": {
                            "type": "string",
                            "description": "File encoding (default: utf-8)",
                            "default": "utf-8",
                        },
                        "tail_lines": {
                            "type": "integer",
                            "description": "Only read last N lines (for log files)",
                        },
                    },
                    "required": ["path"],
                },
                server_name=self.name,
            ),
            MCPTool(
                name="list_directory",
                description="List directory contents (restricted to whitelisted directories)",
                input_schema={
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Absolute directory path",
                        },
                        "pattern": {
                            "type": "string",
                            "description": "Glob pattern filter (e.g., *.log)",
                        },
                        "recursive": {
                            "type": "boolean",
                            "description": "Include subdirectories (default: false)",
                            "default": False,
                        },
                        "limit": {
                            "type": "integer",
                            "description": "Max results (default: 100)",
                            "default": 100,
                        },
                    },
                    "required": ["path"],
                },
                server_name=self.name,
            ),
            MCPTool(
                name="search_in_file",
                description="Search for pattern in file contents",
                input_schema={
                    "type": "object",
                    "properties": {
                        "path": {
                            "type": "string",
                            "description": "Absolute file path",
                        },
                        "pattern": {
                            "type": "string",
                            "description": "Search pattern (regex supported)",
                        },
                        "case_sensitive": {
                            "type": "boolean",
                            "description": "Case sensitive search (default: false)",
                            "default": False,
                        },
                        "context_lines": {
                            "type": "integer",
                            "description": "Lines of context around matches (default: 2)",
                            "default": 2,
                        },
                    },
                    "required": ["path", "pattern"],
                },
                server_name=self.name,
            ),
        ]

    async def execute(
        self,
        tool_name: str,
        parameters: dict[str, Any],
    ) -> MCPToolResult:
        execution_id = str(uuid.uuid4())[:8]

        try:
            if tool_name == "read_file":
                output = await self._read_file(parameters)
            elif tool_name == "list_directory":
                output = await self._list_directory(parameters)
            elif tool_name == "search_in_file":
                output = await self._search_in_file(parameters)
            else:
                return MCPToolResult(
                    success=False,
                    execution_id=execution_id,
                    error=f"Unknown tool: {tool_name}",
                )

            logger.info(
                "filesystem_tool_executed",
                tool=tool_name,
                execution_id=execution_id,
                # 不記錄 path 以避免資訊洩漏
            )

            return MCPToolResult(
                success=True,
                execution_id=execution_id,
                output=output,
            )

        except FilesystemSecurityError as e:
            logger.warning(
                "filesystem_security_violation",
                tool=tool_name,
                execution_id=execution_id,
                error=str(e),
            )
            return MCPToolResult(
                success=False,
                execution_id=execution_id,
                error=f"Security violation: {e}",
            )
        except FileNotFoundError as e:
            return MCPToolResult(
                success=False,
                execution_id=execution_id,
                error=f"File not found: {e}",
            )
        except PermissionError as e:
            return MCPToolResult(
                success=False,
                execution_id=execution_id,
                error=f"Permission denied: {e}",
            )
        except Exception as e:
            logger.exception(
                "filesystem_provider_error",
                tool=tool_name,
                execution_id=execution_id,
                error=str(e),
            )
            return MCPToolResult(
                success=False,
                execution_id=execution_id,
                error=str(e),
            )

    # =========================================================================
    # Security Methods
    # =========================================================================

    def _validate_path(self, path_str: str) -> Path:
        """
        驗證並正規化路徑

        Security checks:
        1. 必須是絕對路徑
        2. 解析 symlink 後必須在白名單目錄內
        3. 不能包含 .. 遍歷
        4. 不能匹配敏感文件 pattern

        Args:
            path_str: 原始路徑字串

        Returns:
            Path: 正規化後的 Path 物件

        Raises:
            FilesystemSecurityError: 路徑違反安全規則
        """
        # 1. 檢查是否為絕對路徑
        if not path_str.startswith("/"):
            raise FilesystemSecurityError(f"Path must be absolute: {path_str}")

        # 2. 檢查 .. 遍歷 (在 resolve 之前)
        if ".." in path_str:
            raise FilesystemSecurityError(f"Path traversal not allowed: {path_str}")

        # 3. 正規化路徑 (解析 symlink)
        path = Path(path_str)
        try:
            resolved_path = path.resolve(strict=False)
        except Exception as e:
            raise FilesystemSecurityError(f"Invalid path: {e}") from e

        # 4. 檢查是否在白名單目錄內
        resolved_str = str(resolved_path)
        in_whitelist = False
        for allowed_dir in self._allowed_directories:
            if resolved_str.startswith(allowed_dir):
                in_whitelist = True
                break

        if not in_whitelist:
            raise FilesystemSecurityError(
                f"Path not in allowed directories: {resolved_str}. "
                f"Allowed: {self._allowed_directories}"
            )

        # 5. 檢查是否為敏感文件
        if self._is_sensitive_file(resolved_path):
            raise FilesystemSecurityError(
                f"Access to sensitive file denied: {resolved_path.name}"
            )

        return resolved_path

    def _is_sensitive_file(self, path: Path) -> bool:
        """
        檢查是否為敏感文件

        Args:
            path: 檔案路徑

        Returns:
            bool: True 如果是敏感文件
        """
        filename = path.name
        full_path = str(path)

        for pattern in self._sensitive_patterns:
            if pattern.search(filename) or pattern.search(full_path):
                return True

        return False

    def _check_file_size(self, path: Path) -> int:
        """
        檢查檔案大小

        Args:
            path: 檔案路徑

        Returns:
            int: 檔案大小 (bytes)

        Raises:
            FilesystemSecurityError: 檔案超過大小限制
        """
        try:
            size = path.stat().st_size
        except OSError as e:
            raise FilesystemSecurityError(f"Cannot stat file: {e}") from e

        if size > self._max_file_size:
            raise FilesystemSecurityError(
                f"File too large: {size} bytes (max: {self._max_file_size})"
            )

        return size

    # =========================================================================
    # Tool Implementations
    # =========================================================================

    async def _read_file(self, parameters: dict) -> dict:
        """
        讀取文件內容

        Args:
            parameters: 包含 path, encoding, tail_lines

        Returns:
            dict: {content, size, path, encoding}
        """
        path_str = parameters.get("path", "")
        encoding = parameters.get("encoding", "utf-8")
        tail_lines = parameters.get("tail_lines")

        if not path_str:
            return {"error": "Missing 'path' parameter"}

        # 安全驗證
        path = self._validate_path(path_str)

        if not path.is_file():
            return {"error": f"Not a file: {path_str}"}

        # 檢查檔案大小
        file_size = self._check_file_size(path)

        # 讀取內容
        try:
            if tail_lines and tail_lines > 0:
                # 只讀最後 N 行 (適合 log 文件)
                content = self._read_last_lines(path, tail_lines, encoding)
            else:
                content = path.read_text(encoding=encoding)
        except UnicodeDecodeError:
            return {"error": f"Cannot decode file with encoding: {encoding}"}

        return {
            "path": str(path),
            "content": content,
            "size": file_size,
            "encoding": encoding,
            "lines": content.count("\n") + 1,
        }

    def _read_last_lines(self, path: Path, n: int, encoding: str = "utf-8") -> str:
        """
        讀取檔案最後 N 行 (適合大型 log 文件)

        使用 seek 從檔案尾部讀取，效率更高
        """
        lines = []
        block_size = 8192

        with open(path, "rb") as f:
            # 移到檔案尾部
            f.seek(0, os.SEEK_END)
            file_size = f.tell()

            # 從尾部逐塊讀取
            remaining = file_size
            buffer = b""

            while len(lines) <= n and remaining > 0:
                read_size = min(block_size, remaining)
                remaining -= read_size
                f.seek(remaining)
                block = f.read(read_size)
                buffer = block + buffer

                # 計算行數
                lines = buffer.split(b"\n")

        # 取最後 N 行
        result_lines = lines[-n:]
        return b"\n".join(result_lines).decode(encoding)

    async def _list_directory(self, parameters: dict) -> dict:
        """
        列出目錄內容

        Args:
            parameters: 包含 path, pattern, recursive, limit

        Returns:
            dict: {path, entries: [{name, type, size, modified}]}
        """
        path_str = parameters.get("path", "")
        pattern = parameters.get("pattern", "*")
        recursive = parameters.get("recursive", False)
        limit = parameters.get("limit", 100)

        if not path_str:
            return {"error": "Missing 'path' parameter"}

        # 安全驗證
        path = self._validate_path(path_str)

        if not path.is_dir():
            return {"error": f"Not a directory: {path_str}"}

        # 列出內容
        entries = []
        try:
            if recursive:
                items = list(path.rglob(pattern))
            else:
                items = list(path.glob(pattern))

            # 排序並限制數量
            items = sorted(items, key=lambda p: p.name)[:limit]

            for item in items:
                # 跳過敏感文件
                if self._is_sensitive_file(item):
                    continue

                try:
                    stat = item.stat()
                    entries.append({
                        "name": item.name,
                        "path": str(item),
                        "type": "directory" if item.is_dir() else "file",
                        "size": stat.st_size if item.is_file() else None,
                        "modified": stat.st_mtime,
                    })
                except (PermissionError, OSError):
                    # 無權限的項目跳過
                    continue

        except PermissionError:
            return {"error": f"Permission denied: {path_str}"}

        return {
            "path": str(path),
            "pattern": pattern,
            "recursive": recursive,
            "count": len(entries),
            "entries": entries,
        }

    async def _search_in_file(self, parameters: dict) -> dict:
        """
        在文件中搜尋 pattern

        Args:
            parameters: 包含 path, pattern, case_sensitive, context_lines

        Returns:
            dict: {path, pattern, matches: [{line_number, line, context}]}
        """
        path_str = parameters.get("path", "")
        search_pattern = parameters.get("pattern", "")
        case_sensitive = parameters.get("case_sensitive", False)
        context_lines = parameters.get("context_lines", 2)

        if not path_str:
            return {"error": "Missing 'path' parameter"}
        if not search_pattern:
            return {"error": "Missing 'pattern' parameter"}

        # 安全驗證
        path = self._validate_path(path_str)

        if not path.is_file():
            return {"error": f"Not a file: {path_str}"}

        # 檢查檔案大小
        self._check_file_size(path)

        # 編譯搜尋 pattern
        flags = 0 if case_sensitive else re.IGNORECASE
        try:
            regex = re.compile(search_pattern, flags)
        except re.error as e:
            return {"error": f"Invalid regex pattern: {e}"}

        # 搜尋內容
        matches = []
        try:
            lines = path.read_text().splitlines()

            for i, line in enumerate(lines):
                if regex.search(line):
                    # 取得前後 context
                    start = max(0, i - context_lines)
                    end = min(len(lines), i + context_lines + 1)
                    context = lines[start:end]

                    matches.append({
                        "line_number": i + 1,  # 1-based
                        "line": line,
                        "context": context,
                        "context_start": start + 1,
                    })

                    # 限制結果數量
                    if len(matches) >= self._max_search_results:
                        break

        except UnicodeDecodeError:
            return {"error": "File is not valid UTF-8 text"}

        return {
            "path": str(path),
            "pattern": search_pattern,
            "case_sensitive": case_sensitive,
            "match_count": len(matches),
            "truncated": len(matches) >= self._max_search_results,
            "matches": matches,
        }

    async def health_check(self) -> bool:
        """
        健康檢查

        驗證至少一個白名單目錄存在且可讀
        """
        for dir_path in self._allowed_directories:
            path = Path(dir_path)
            if path.exists() and path.is_dir():
                try:
                    # 嘗試列出目錄驗證權限
                    list(path.iterdir())
                    return True
                except PermissionError:
                    continue
        return False
