"""Database manager for filescan - 扫描结果持久化."""

from __future__ import annotations

import sqlite3
from pathlib import Path
from typing import Any

from pytola_core import DatabaseManager

from .fileinfo import FileInfo, MatchInfo
from .logger import logger


class ScanDatabase(DatabaseManager):
    """扫描结果 SQLite 数据库管理器.

    特性:
    - 线程安全的数据库操作
    - 连接池管理
    - 自动表结构初始化
    - 事务支持
    """

    def __init__(self, db_path: Path | str, max_connections: int = 10) -> None:
        """初始化数据库连接.

        Args:
            db_path: SQLite 数据库文件路径
            max_connections: 连接池最大连接数
        """
        from pytola_core.persistance.database import PoolConfig

        pool_config = PoolConfig(max_size=max_connections)
        super().__init__(db_path=db_path, pool_config=pool_config)

        # 初始化表结构, 使用连接池获取连接
        try:
            with self.pool.get_connection() as conn:
                cursor = conn.cursor()
                self._create_tables(cursor)
        except (sqlite3.Error, OSError) as e:
            logger.error(f"初始化数据库表结构失败:{e}", exc_info=True)
            raise

    def _create_tables(self, cursor: sqlite3.Cursor) -> None:
        """如果不存在则创建数据库表.

        Args:
            cursor: SQLite 游标
        """
        # 目录文件信息表 - 保存目录下所有文件的基本信息
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS directory_files (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                root_path TEXT NOT NULL,
                file_path TEXT NOT NULL,
                file_type TEXT,
                file_size INTEGER,
                creation_time TEXT,
                modification_time TEXT,
                is_skipped INTEGER DEFAULT 0,
                skip_reason TEXT,
                scan_status TEXT DEFAULT 'pending',
                metadata TEXT,
                file_level INTEGER DEFAULT 0,
                filename_multi_level INTEGER DEFAULT 0,
                file_content_level INTEGER DEFAULT 0,
                matches TEXT DEFAULT '[]'
            )
        """)

        # 检查并添加缺失的列, 用于旧版本数据库升级
        self._migrate_directory_files_table(cursor)

        # 匹配结果表 - 保存文件匹配结果
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS file_matches (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                file_id INTEGER NOT NULL,
                rule_name TEXT NOT NULL,
                rule_description TEXT,
                match_type TEXT,
                analysis_type TEXT,
                line_number INTEGER,
                match_text TEXT,
                start_pos INTEGER,
                end_pos INTEGER,
                context_lines TEXT,
                is_skipped INTEGER DEFAULT 0,
                skip_reason TEXT,
                metadata TEXT,
                FOREIGN KEY (file_id) REFERENCES directory_files(id)
            )
        """)

    def _migrate_directory_files_table(self, cursor: sqlite3.Cursor) -> None:
        """迁移 directory_files 表，添加缺失的列.

        Args:
            cursor: SQLite 游标
        """
        # 获取当前表的所有列名
        cursor.execute("PRAGMA table_info(directory_files)")
        existing_columns = {row[1] for row in cursor.fetchall()}

        # 需要添加的列及其默认值
        columns_to_add = [
            ("file_level", "INTEGER DEFAULT 0"),
            ("filename_multi_level", "INTEGER DEFAULT 0"),
            ("file_content_level", "INTEGER DEFAULT 0"),
            ("matches", "TEXT DEFAULT '[]'"),
            ("processing_time_seconds", "REAL DEFAULT 0"),
            ("match_count", "INTEGER DEFAULT 0"),
            ("analyse_status", "TEXT DEFAULT 'pending'"),
        ]

        # 检查并添加缺失的列
        for column_name, column_type in columns_to_add:
            if column_name not in existing_columns:
                logger.debug(f"添加缺失的列:{column_name}")
                # 安全验证：确保列名和类型只包含允许的字符，防止 SQL 注入
                # 列名只能包含字母、数字和下划线
                if not all(c.isalnum() or c == "_" for c in column_name):
                    msg = f"无效的列名: {column_name}"
                    raise ValueError(msg)
                # 列类型只能包含字母、数字、空格、单引号、括号和下划线
                if not all(c.isalnum() or c in " ()_'\"" for c in column_type):
                    msg = f"无效的列类型: {column_type}"
                    raise ValueError(msg)
                # SQLite 的 ALTER TABLE 语句不支持参数化，使用白名单验证确保安全
                cursor.execute(
                    f"ALTER TABLE directory_files ADD COLUMN {column_name} {column_type}",
                )

    def save_directory_files(self, files_info: list[FileInfo]) -> int:
        """保存目录下的所有文件信息到数据库.

        Args:
            files_info: 文件信息列表,可以是字典或 DirFileInfo 对象:
                - root_path: 根路径
                - file_path: 文件路径
                - file_type: 文件类型
                - file_size: 文件大小
                - creation_time: 创建时间
                - modification_time: 修改时间
                - is_skipped: 是否跳过扫描
                - skip_reason: 跳过原因
                - scan_status: 扫描状态 ('pending', 'scanning', 'completed', 'error')
                - metadata: 其他元数据

        Returns
        -------
            成功插入的文件数量
        """
        try:
            with self.transaction() as conn:
                cursor = conn.cursor()

                if not files_info:
                    return 0

                # 先获取第一个文件的 root_path 用于清理旧数据
                first_file = files_info[0]
                if isinstance(first_file, FileInfo):
                    root_path = first_file.root_path
                else:
                    root_path = first_file.get("root_path", "")

                # 如果是 DirectoryFileInfo 对象,先删除该 root_path 的旧记录
                if root_path and isinstance(first_file, FileInfo):
                    logger.debug(f"清理 root_path={root_path} 的旧记录")
                    cursor.execute("DELETE FROM directory_files WHERE root_path = ?", (root_path,))

                columns = FileInfo.columns()
                # 安全验证：确保所有列名都是合法的标识符，防止 SQL 注入
                for col in columns:
                    if not all(c.isalnum() or c == "_" for c in col):
                        msg = f"无效的列名: {col}"
                        raise ValueError(msg)
                placeholders = ", ".join(["?" for _ in columns])
                query = f"INSERT INTO directory_files ({', '.join(columns)}) VALUES ({placeholders})"  # noqa: S608

                inserted_count = 0
                for file_info in files_info:
                    if not isinstance(file_info, FileInfo):
                        logger.warning(f"跳过非 DirFileInfo 对象:{file_info}")
                        continue

                    # 注意:columns() 不包含 id,所以 values 中也不需要 id
                    values = [
                        file_info.root_path,
                        file_info.file_path,
                        file_info.file_type,
                        file_info.file_size,
                        file_info.creation_time,
                        file_info.modification_time,
                        file_info.is_skipped,
                        file_info.skip_reason,
                        file_info.scan_status,
                        file_info.metadata or "{}",
                        file_info.file_level,
                        file_info.filename_multi_level,
                        file_info.file_content_level,
                        file_info.matches if isinstance(file_info.matches, str) else str(file_info.matches),
                        file_info.processing_time_seconds,
                        file_info.match_count,
                        file_info.analyse_status,
                    ]
                    cursor.execute(query, values)
                    inserted_count += 1

                return inserted_count

        except sqlite3.Error as e:
            msg = f"保存目录文件信息失败:{e}"
            raise RuntimeError(msg) from e

    def get_directory_files(self, root_path: str) -> list[FileInfo]:
        """获取指定会话的目录文件信息列表.

        Args:
            root_path: 根路径

        Returns
        -------
            文件信息字典列表
        """
        dict_data = self.query(
            """
            SELECT * FROM directory_files
            WHERE root_path = ?
            ORDER BY file_path
        """,
            (root_path,),
        )
        return [FileInfo.from_dict(d) for d in dict_data]

    def get_directory_files_by_root_path(self, limit: int = 1) -> list[FileInfo]:
        """获取数据库中的任意文件信息(用于获取根路径).

        Args:
            limit: 返回的记录数量限制

        Returns
        -------
            文件信息列表
        """
        dict_data = self.query(
            """
            SELECT * FROM directory_files
            LIMIT ?
        """,
            (limit,),
        )
        return [FileInfo.from_dict(d) for d in dict_data]

    def get_all_directory_files(self) -> list[FileInfo]:
        """获取数据库中所有的文件信息(不限制 root_path,按 file_path 去重).

        Returns
        -------
            文件信息列表
        """
        # 使用 GROUP BY file_path 去重,确保每个文件路径只返回一条记录
        dict_data = self.query(
            """
            SELECT * FROM directory_files
            GROUP BY file_path
            ORDER BY file_path
        """,
        )
        return [FileInfo.from_dict(d) for d in dict_data]

    def get_skipped_files(self, root_path: str) -> list[dict[str, Any]]:
        """获取指定会话中被跳过的文件列表.

        Args:
            session_id: 会话 ID

        Returns
        -------
            被跳过的文件信息字典列表
        """
        return self.query(
            """
            SELECT * FROM directory_files
            WHERE root_path = ? AND is_skipped = 1
            ORDER BY file_path
        """,
            (root_path,),
        )

    def get_file_by_path(self, root_path: str, file_path: str) -> dict[str, Any] | None:
        """根据文件路径获取文件信息.

        Args:
            session_id: 会话 ID
            file_path: 文件路径

        Returns
        -------
            文件信息字典,如果没有找到则返回 None
        """
        return self.query_one(
            """
            SELECT * FROM directory_files
            WHERE root_path = ? AND file_path = ?
        """,
            (root_path, file_path),
        )

    def save_file_matches(self, matches: list[MatchInfo]) -> int:
        """保存文件匹配结果到数据库.

        Args:
            matches: 匹配结果列表

        Returns
        -------
            成功插入的匹配数量
        """
        try:
            with self.transaction() as conn:
                cursor = conn.cursor()

                if not matches:
                    return 0

                columns = MatchInfo.columns()
                # 安全验证：确保所有列名都是合法的标识符，防止 SQL 注入
                for col in columns:
                    if not all(c.isalnum() or c == "_" for c in col):
                        msg = f"无效的列名:{col}"
                        raise ValueError(msg)
                placeholders = ", ".join(["?" for _ in columns])
                query = f"INSERT INTO file_matches ({', '.join(columns)}) VALUES ({placeholders})"  # noqa: S608

                inserted_count = 0
                for match_info in matches:
                    if not isinstance(match_info, MatchInfo):
                        logger.warning(f"跳过非 MatchInfo 对象:{match_info}")
                        continue

                    values = [
                        match_info.file_id,
                        match_info.rule_name,
                        match_info.rule_description,
                        match_info.match_type,
                        match_info.analysis_type,
                        match_info.line_number,
                        match_info.match_text,
                        match_info.start_pos,
                        match_info.end_pos,
                        match_info.context_lines
                        if isinstance(match_info.context_lines, str)
                        else str(match_info.context_lines),
                        match_info.is_skipped,
                        match_info.skip_reason,
                        match_info.metadata or "{}",
                    ]
                    cursor.execute(query, values)
                    inserted_count += 1

                return inserted_count

        except sqlite3.Error as e:
            msg = f"保存匹配结果失败:{e}"
            raise RuntimeError(msg) from e

    def get_file_matches(self, file_id: int) -> list[MatchInfo]:
        """获取指定文件的匹配结果.

        Args:
            file_id: 文件 ID

        Returns
        -------
            匹配结果列表
        """
        dict_data = self.query(
            """
            SELECT * FROM file_matches
            WHERE file_id = ?
            ORDER BY analysis_type, rule_name, line_number
        """,
            (file_id,),
        )
        return [MatchInfo.from_dict(d) for d in dict_data]

    def get_all_matches(self) -> list[MatchInfo]:
        """获取数据库中所有的匹配结果.

        Returns
        -------
            匹配结果列表
        """
        dict_data = self.query(
            """
            SELECT * FROM file_matches
            ORDER BY file_id, analysis_type, rule_name, line_number
        """,
        )
        return [MatchInfo.from_dict(d) for d in dict_data]

    def update_file_analyse_status(
        self,
        file_id: int,
        analyse_status: str,
        processing_time_seconds: float = 0.0,
        match_count: int = 0,
        matches_json: str = "[]",
    ) -> bool:
        """更新单个文件的分析状态和结果.

        Args:
            file_id: 文件 ID
            analyse_status: 分析状态 (pending, analysing, completed, error)
            processing_time_seconds: 处理耗时(秒)
            match_count: 匹配数量
            matches_json: 匹配结果的 JSON 字符串

        Returns
        -------
            是否更新成功
        """
        try:
            with self.transaction() as conn:
                cursor = conn.cursor()
                cursor.execute(
                    """
                    UPDATE directory_files
                    SET analyse_status = ?,
                        processing_time_seconds = ?,
                        match_count = ?,
                        matches = ?
                    WHERE id = ?
                """,
                    (analyse_status, processing_time_seconds, match_count, matches_json, file_id),
                )
                return cursor.rowcount > 0
        except sqlite3.Error as e:
            logger.error(f"更新文件分析状态失败:{e}")
            return False

    def ensure_file_exists_or_insert(
        self,
        root_path: str,
        file_path: str,
        file_type: str = "",
        file_size: int = 0,
        creation_time: str | None = None,
        modification_time: str | None = None,
        is_skipped: int = 0,
        skip_reason: str | None = None,
        scan_status: str = "pending",
        metadata: str = "{}",
        file_level: int = 0,
        filename_multi_level: int = 0,
        file_content_level: int = 0,
        matches: list[int] | None = None,
        processing_time_seconds: float = 0.0,
        match_count: int = 0,
        analyse_status: str = "pending",
    ) -> int | None:
        """确保文件记录存在,如果不存在则插入,并返回文件 ID.

        Args:
            root_path: 根路径
            file_path: 文件路径
            file_type: 文件类型
            file_size: 文件大小
            creation_time: 创建时间
            modification_time: 修改时间
            is_skipped: 是否跳过
            skip_reason: 跳过原因
            scan_status: 扫描状态
            metadata: 元数据(JSON 字符串)
            file_level: 文件级别
            filename_multi_level: 文件名多级匹配级别
            file_content_level: 文件内容匹配级别
            matches: 匹配列表(JSON 字符串)
            processing_time_seconds: 处理耗时
            match_count: 匹配数量
            analyse_status: 分析状态

        Returns
        -------
            文件 ID,如果失败则返回 None
        """
        try:
            with self.transaction() as conn:
                cursor = conn.cursor()

                # 先尝试根据 file_path 查找现有记录
                cursor.execute(
                    "SELECT id FROM directory_files WHERE file_path = ? AND root_path = ?",
                    (file_path, root_path),
                )
                row = cursor.fetchone()

                if row:
                    # 文件已存在,更新分析状态和性能指标
                    cursor.execute(
                        """
                        UPDATE directory_files
                        SET analyse_status = ?,
                            processing_time_seconds = ?,
                            match_count = ?
                        WHERE id = ?
                    """,
                        (analyse_status, processing_time_seconds, match_count, row[0]),
                    )
                    logger.debug(f"文件已存在,更新分析状态:{file_path} (ID={row[0]})")
                    return row[0]
                # 文件不存在,插入新记录
                cursor.execute(
                    """
                        INSERT INTO directory_files (
                            root_path, file_path, file_type, file_size,
                            creation_time, modification_time, is_skipped, skip_reason,
                            scan_status, metadata, file_level, filename_multi_level,
                            file_content_level, matches, processing_time_seconds,
                            match_count, analyse_status
                        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    """,
                    (
                        root_path,
                        file_path,
                        file_type,
                        file_size,
                        creation_time,
                        modification_time,
                        is_skipped,
                        skip_reason,
                        scan_status,
                        metadata,
                        file_level,
                        filename_multi_level,
                        file_content_level,
                        str(matches) if matches else "[]",
                        processing_time_seconds,
                        match_count,
                        analyse_status,
                    ),
                )
                new_id = cursor.lastrowid
                logger.debug(f"插入新文件记录:{file_path} (ID={new_id})")
                return new_id

        except sqlite3.Error as e:
            logger.error(f"确保文件记录存在失败:{e}")
            return None

    def save_analyse_results(self, results: dict[str, Any]) -> int:
        """保存完整的扫描结果到数据库(包括文件信息和匹配结果).

        Args:
            results: 扫描结果字典,包含 scan_info 和 matches
                - scan_info: 扫描统计信息
                - matches: 匹配结果列表
                - rules: 使用的规则列表

        Returns
        -------
            成功保存的匹配记录数量
        """
        try:
            matches_data = results.get("matches", [])
            if not matches_data:
                logger.info("没有匹配结果需要保存")
                return 0

            # 转换 MatchInfo 对象为列表(如果还不是对象)
            match_objects = []
            for match_data in matches_data:
                if isinstance(match_data, MatchInfo):
                    match_objects.append(match_data)
                elif isinstance(match_data, dict):
                    match_objects.append(MatchInfo.from_dict(match_data))
                else:
                    logger.warning(f"跳过无效的匹配数据:{match_data}")
                    continue
            saved_count = self.save_file_matches(match_objects)
        except (sqlite3.Error, OSError, ValueError) as e:
            msg = f"保存扫描结果失败:{e}"
            raise RuntimeError(msg) from e
        else:
            logger.info(f"保存了 {saved_count} 条匹配记录")
            return saved_count
