# Anki Add-on Builder
#
# Copyright (C)  2016-2021 Aristotelis P. <https://glutanimate.com/>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version, with the additions
# listed at the end of the license file that accompanied this program.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# NOTE: This program is subject to certain additional terms pursuant to
# Section 7 of the GNU Affero General Public License.  You should have
# received a copy of these additional terms immediately following the
# terms and conditions of the GNU Affero General Public License that
# accompanied this program.
#
# If not, please request a copy through one of the means of contact
# listed here: <https://glutanimate.com/contact/>.
#
# Any modifications to this file must keep this entire header intact.

"""
Qt6 UI Compilation

Compiles Qt Designer .ui files to Python classes using pyuic6.
This module focuses exclusively on Qt6 support for modern Anki add-ons.
"""

import logging
import re
import shutil
import subprocess
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any

from whichcraft import which

from aadt import __title__, __version__
from aadt.config import Config

# Default path names (kept for backward compatibility)
DEFAULT_UI_FOLDER_NAME = "ui"
DEFAULT_DESIGNER_FOLDER_NAME = "designer"
DEFAULT_RESOURCES_FOLDER_NAME = "resources"
DEFAULT_FORMS_PACKAGE_NAME = "forms"


class UICompilationError(Exception):
    """Exception raised when UI compilation fails"""

    def __init__(self, message: str, ui_file: Path | None = None, stderr: str | None = None):
        self.ui_file = ui_file
        self.stderr = stderr
        super().__init__(message)


_template_header = '''\
# -*- coding: utf-8 -*-
#
# {display_name} Add-on for Anki
# Copyright (C)  {years} {author}{contact}
#
# This file was automatically generated by {__title__} v{__version__}
# It is subject to the same licensing terms as the rest of the program
# (see the LICENSE file which accompanies this program).
#
# WARNING! All changes made in this file will be lost!

"""
Initializes generated Qt forms/resources
"""\
'''

_template_all = """\
__all__ = [
{}
]\
"""

_template_qt_shim = '''\
# -*- coding: utf-8 -*-
#
# {display_name} Add-on for Anki
# Copyright (C)  {years} {author}{contact}
#
# This file was automatically generated by {__title__} v{__version__}
# It is subject to the same licensing terms as the rest of the program
# (see the LICENSE file which accompanies this program).
#
# WARNING! All changes made in this file will be lost!

"""
Imports Qt6 forms directly for modern Anki add-ons
"""

from .qt6 import *  # noqa: F401
'''


class QtVersion(Enum):
    """Qt version enum - Qt6 only for modern add-ons"""

    qt6 = 6


class UIBuilder:
    """
    Modern Qt6-only UI builder for Anki add-ons.

    Compiles .ui files from Qt Designer into Python classes using pyuic6.
    No longer supports Qt5 or QRC resource files - uses modern Qt6 approach.
    """

    _re_munge = re.compile(r"^import .+?_rc(\n)?$", re.MULTILINE)
    _re_pyqt6_import = re.compile(r"^from PyQt6 import (.+)$", re.MULTILINE)
    _ui_file_glob = "*.ui"
    _ui_file_tool = "pyuic"

    def __init__(self, dist: Path, config: Config) -> None:
        self._dist = dist
        self._config = config
        self._addon_config = config.as_dataclass()

        # Get UI configuration
        ui_config = self._addon_config.build_config.ui_config

        self._gui_path: Path = self._dist / "src" / self._addon_config.module_name / "gui"
        self._ui_source_path: Path = self._dist / ui_config.ui_dir
        self._forms_source_path: Path = self._ui_source_path / ui_config.designer_dir
        self._resources_source_path: Path = self._ui_source_path / ui_config.resources_dir
        self._forms_out_path: Path = self._gui_path / ui_config.forms_package

        self._format_dict: dict[str, Any] = self._get_format_dict()

    def _compile_ui_file(self, ui_file: Path, output_file: Path, tool: str) -> None:
        """Compile a single UI file with improved error handling"""
        try:
            # Use subprocess.run for better error control
            result = subprocess.run(  # noqa: S603
                [tool, str(ui_file), "-o", str(output_file)],
                capture_output=True,
                text=True,
                check=True,
                timeout=30,  # 30 second timeout
            )

            if result.stderr:
                logging.debug(f"pyuic6 stderr for {ui_file.name}: {result.stderr}")

        except subprocess.CalledProcessError as e:
            error_msg = f"Failed to compile {ui_file.name}"
            if e.stderr:
                error_msg += f": {e.stderr.strip()}"
            raise UICompilationError(error_msg, ui_file=ui_file, stderr=e.stderr) from e
        except subprocess.TimeoutExpired as e:
            raise UICompilationError(f"UI compilation timed out for {ui_file.name}", ui_file=ui_file) from e
        except FileNotFoundError as e:
            raise UICompilationError(
                f"Tool {tool} not found. Please ensure PyQt6 is installed.",
                ui_file=ui_file,
            ) from e

    def build(self) -> bool:
        qt_version = QtVersion.qt6
        qt_version_key = qt_version.name

        logging.info("Starting UI build tasks for target %r...", qt_version_key)

        path_in = self._forms_source_path
        path_out = self._forms_out_path / qt_version_key

        # Qt6 doesn't need QRC resource migration
        resource_prefixes_to_replace: list[str] = []

        if not path_in.exists():
            logging.warning(f"No Qt forms folder found under {self._forms_source_path}. Skipping build.")
            return False

        ret = self._build(
            path_in=path_in,
            path_out=path_out,
            qt_version_number=qt_version.value,
            resource_prefixes_to_replace=resource_prefixes_to_replace,
        )

        # Copy UI resources to the module
        if self._resources_source_path.exists():
            self._copy_ui_resources()

        logging.info("Done with all UI build tasks.")

        return ret

    def create_qt_shim(self) -> bool:
        if not self._forms_out_path.is_dir():
            return False
        out_path = self._forms_out_path / "__init__.py"
        if out_path.exists():
            out_path.unlink()
        format_dict = self._format_dict
        content = _template_qt_shim.format(**format_dict)
        with out_path.open("w", encoding="utf-8") as f:
            f.write(content)
        return True

    def _build(
        self,
        path_in: Path,
        path_out: Path,
        qt_version_number: int,
        resource_prefixes_to_replace: list[str],
    ) -> bool:
        # Basic checks
        ui_files = list(path_in.glob(self._ui_file_glob))
        if not ui_files:
            logging.warning("No forms found in %s. Skipping UI build.", path_in)
            return False

        # Find a valid UIC tool, preferring pyside6-uic
        tool_names = [f"pyside{qt_version_number}-uic", f"pyuic{qt_version_number}"]
        tool_path = None
        for name in tool_names:
            path = which(name)
            if path:
                tool_path = path
                break

        if not tool_path:
            tools_list = ", ".join(tool_names)
            logging.error(f"Could not find any of the required UI compilation tools: {tools_list}")
            logging.error("PyQt6 should be available through AADT. Try: uv sync --group dev")
            return False

        logging.info("Using UI compiler: %s", tool_path)
        path_out.mkdir(parents=True, exist_ok=True)

        compiled_modules: list[str] = []

        for file in ui_files:
            out_name = f"form_{file.stem.lower().replace('-', '_')}.py"
            out_file = path_out / out_name
            try:
                self._compile_ui_file(file, out_file, str(tool_path))
                self._munge_form(out_file, resource_prefixes_to_replace)
                compiled_modules.append(out_name.replace(".py", ""))
            except UICompilationError as e:
                ui_file_str = self._relative_to_cwd(e.ui_file) if e.ui_file else "unknown file"
                logging.error(
                    f"Error compiling {ui_file_str}: {e}",
                )
                if e.stderr:
                    logging.error("Compiler output:\n%s", e.stderr)
                # Continue to next file
            except Exception as e:
                logging.error("An unexpected error occurred during UI compilation: %s", e)

        if compiled_modules:
            self._write_init_file(compiled_modules, path_out)
            return True

        return False

    def _write_init_file(self, modules: list[str], path_out: Path) -> None:
        logging.debug("Generating init file for %s", self._relative_to_cwd(path_out))

        header = _template_header.format(**self._format_dict)
        all_str = self._generate_all_str(modules)
        import_str = self._generate_import_str(modules)

        init = "\n\n".join((header, all_str, import_str)) + "\n"

        with (path_out / "__init__.py").open("w", encoding="utf-8") as f:
            f.write(init)

    def _generate_all_str(self, modules: list[str]) -> str:
        module_string = ",\n".join(f'    "{m}"' for m in modules)
        out = _template_all.format(module_string)
        return out

    def _generate_import_str(self, modules: list[str]) -> str:
        out = "\n".join(f"from . import {m}" for m in modules)
        return out

    def _munge_form(self, path: Path, resource_prefixes_to_replace: list[str]) -> None:
        """
        Post-process generated Qt form files to clean up resources and imports.

        This method performs three main operations:
        1. Removes `import *_rc` statements generated by pyuic6 for QRC resources
        2. Converts Qt resource syntax (:/prefix/path) to direct file paths
        3. Replaces PyQt6 imports with Anki-compatible aqt.qt imports

        Args:
            path: Path to the generated Python form file
            resource_prefixes_to_replace: List of resource prefixes to convert
        """
        logging.debug("Munging %s...", self._relative_to_cwd(path))
        with path.open("r+", encoding="utf-8") as f:
            form = f.read()

            # Remove QRC import statements
            munged = self._re_munge.sub("", form)

            # Convert resource prefixes
            for prefix in resource_prefixes_to_replace:
                munged = munged.replace(f'":/{prefix}/', f'"{prefix}:')

            # Replace PyQt6 imports with aqt.qt imports for Anki compatibility
            munged = self._convert_pyqt6_imports(munged)

            f.seek(0)
            f.write(munged)
            f.truncate()

    def _convert_pyqt6_imports(self, content: str) -> str:
        """
        Convert PyQt6 imports to Anki-compatible aqt.qt imports.

        pyuic6 generates: from PyQt6 import QtCore, QtGui, QtWidgets
        But aqt.qt only exports individual classes, not module names.

        This method:
        1. Finds PyQt6 import statements
        2. Extracts used Qt classes from the code
        3. Replaces with direct imports from aqt.qt
        4. Removes module prefixes (e.g., QtWidgets.QVBoxLayout -> QVBoxLayout)
        5. Adds type annotations to function definitions
        """
        # Check if there's a PyQt6 import to replace
        pyqt6_match = self._re_pyqt6_import.search(content)
        if not pyqt6_match:
            return content

        # Extract modules being imported (e.g., "QtCore, QtGui, QtWidgets")
        imported_modules = [m.strip() for m in pyqt6_match.group(1).split(",")]

        # Find all Qt class usages in the code
        used_classes = set()
        for module in imported_modules:
            # Pattern to match ModuleName.ClassName usage
            pattern = rf"{module}\.(\w+)"
            matches = re.findall(pattern, content)
            used_classes.update(matches)

        # Determine the widget type for annotations and ensure it's imported
        widget_type = self._infer_widget_type(content)
        used_classes.add(widget_type)

        if not used_classes:
            # If no specific classes found, use wildcard import
            replacement = "from aqt.qt import *"
        else:
            # Import only the classes that are actually used
            sorted_classes = sorted(used_classes)
            replacement = f"from aqt.qt import {', '.join(sorted_classes)}"

        # Replace the import statement
        content = self._re_pyqt6_import.sub(replacement, content)

        # Remove module prefixes from usage (QtWidgets.QVBoxLayout -> QVBoxLayout)
        for module in imported_modules:
            content = re.sub(rf"{module}\.(\w+)", r"\1", content)

        # Add type annotations to UI methods for mypy compliance
        content = self._add_type_annotations(content)

        return content

    def _add_type_annotations(self, content: str) -> str:
        """
        Add type annotations to pyuic6-generated functions for mypy compliance.

        Uses intelligent type inference based on aqt.qt's actual type information:
        - Analyzes UI class name to determine the most appropriate base type
        - Falls back to QWidget if specific type cannot be determined
        - Ensures all generated code is mypy-compliant with precise types
        """
        # Determine the most appropriate type based on UI class analysis
        widget_type = self._infer_widget_type(content)

        # Add type annotation for setupUi method
        content = re.sub(
            r"def setupUi\(self, ([^)]+)\):",
            rf"def setupUi(self, \1: {widget_type}) -> None:",
            content,
        )

        # Add type annotation for retranslateUi method
        content = re.sub(
            r"def retranslateUi\(self, ([^)]+)\):",
            rf"def retranslateUi(self, \1: {widget_type}) -> None:",
            content,
        )

        return content

    def _infer_widget_type(self, content: str) -> str:
        """
        Intelligently infer the most appropriate Qt widget type based on:
        1. UI class name analysis (Ui_Dialog -> QDialog)
        2. Available types in aqt.qt module
        3. Qt widget inheritance hierarchy

        Returns the most specific type that's available in aqt.qt.
        """
        try:
            # Import aqt.qt to check available types
            import aqt.qt

            # Analyze UI class name to infer base widget type
            class_match = re.search(r"class Ui_(\w+)", content)
            if class_match:
                ui_class_suffix = class_match.group(1)

                # Map common UI class patterns to Qt types
                type_mappings = {
                    "Dialog": "QDialog",
                    "MainWindow": "QMainWindow",
                    "Widget": "QWidget",
                    "Frame": "QFrame",
                    "GroupBox": "QGroupBox",
                    "ScrollArea": "QScrollArea",
                }

                # Check for exact matches first
                for pattern, qt_type in type_mappings.items():
                    if pattern in ui_class_suffix and hasattr(aqt.qt, qt_type):
                        # Verify the type exists in aqt.qt
                        return qt_type

            # Fall back to QWidget (always available in aqt.qt)
            return "QWidget"

        except ImportError:
            # If aqt is not available, fall back to QWidget
            return "QWidget"

    def _get_format_dict(self) -> dict[str, Any]:
        config = self._addon_config
        start_year = config.copyright_start
        now = datetime.now().year
        years = f"{start_year}-{now}" if start_year and start_year != now else f"{now}"

        contact = config.contact

        format_dict = {
            "display_name": config.display_name,
            "author": config.author,
            "contact": "" if not contact else f" <{contact}>",
            "__title__": __title__,
            "__version__": __version__,
            "years": years,
        }

        return format_dict

    def _copy_ui_resources(self) -> None:
        """
        Copy UI resources from ui/resources/ to src/module/gui/resources/ with filtering
        """
        from aadt.utils import copy_recursively

        target_resources_path = self._gui_path / "resources"
        ui_config = self._addon_config.build_config.ui_config

        logging.info("Copying UI resources...")
        logging.debug("From: %s", self._relative_to_cwd(self._resources_source_path))
        logging.debug("To: %s", self._relative_to_cwd(target_resources_path))

        # Create target directory if it doesn't exist
        target_resources_path.mkdir(parents=True, exist_ok=True)

        # Copy all resources with optional filtering
        for item in self._resources_source_path.iterdir():
            # Check if we should exclude optional resources
            if ui_config.exclude_optional_resources and item.is_dir() and item.name == "optional":
                logging.debug("Skipping optional resources directory: %s", item.name)
                continue

            if item.is_file():
                # Copy files directly
                target_file = target_resources_path / item.name
                target_file.write_bytes(item.read_bytes())
                logging.debug("Copied file: %s", item.name)
            elif item.is_dir():
                # Handle optional subdirectories within other directories
                if ui_config.exclude_optional_resources and self._contains_optional_directory(item):
                    self._copy_directory_excluding_optional(item, target_resources_path / item.name)
                else:
                    # Copy directories recursively
                    target_dir = target_resources_path / item.name
                    copy_recursively(str(item), str(target_dir))
                logging.debug("Copied directory: %s", item.name)

        # Ensure resources directory is a Python package for importlib.resources support
        if ui_config.create_resources_package:
            resources_init_file = target_resources_path / "__init__.py"
            if not resources_init_file.exists():
                self._create_resources_init_file(resources_init_file)

    def _create_resources_init_file(self, init_path: Path) -> None:
        """
        Create __init__.py for resources package to support importlib.resources.

        This enables modern Python resource management with full compatibility for:
        - .zip file distribution (Anki add-ons)
        - Cross-platform resource access
        - Python 3.9+ standard library support
        """
        content = f'''\
"""
{self._addon_config.display_name}资源管理 - 现代Python最佳实践

使用importlib.resources进行资源访问, 支持:
- 打包兼容性(.zip文件分发)
- 平台无关性
- Python 3.9+ 标准库支持
- 完美兼容Anki插件运行环境
"""

from importlib import resources
from pathlib import Path


def get_icon_path(icon_name: str) -> str:
    """
    获取图标文件的完整路径 - 现代Python资源管理

    支持多种部署方式:
    - 开发环境: 直接文件访问
    - 生产环境: .zip打包分发
    - Anki插件: 包内资源访问

    Args:
        icon_name: 图标文件名(如 "wait.png")

    Returns:
        图标文件的完整路径字符串, 如果不存在返回空字符串
    """
    try:
        # Python 3.9+ 推荐的 importlib.resources 语法
        files = resources.files(__package__)
        icon_file = files / icon_name
        if icon_file.is_file():
            return str(icon_file)

        # 降级方案: pathlib(开发环境)
        fallback_path = Path(__file__).parent / icon_name
        if fallback_path.exists():
            return str(fallback_path)

    except Exception as e:
        # 使用Anki标准日志系统记录错误但不中断程序
        from aqt import mw

        if mw and mw.addonManager:
            logger = mw.addonManager.get_logger(__name__)
            logger.warning(f"图标资源获取失败 {{icon_name}}: {{e}}")
        # 对于资源加载错误, 不显示用户界面错误, 只记录日志

    # 返回空字符串, QIcon会创建空图标而不是崩溃
    return ""


def get_resource_path(resource_name: str) -> str | None:
    """
    通用资源路径获取函数

    Args:
        resource_name: 资源文件名

    Returns:
        资源文件路径, 如果不存在返回None
    """
    path = get_icon_path(resource_name)
    return path if path else None
'''
        with init_path.open("w", encoding="utf-8") as f:
            f.write(content)
        logging.debug("Created resources __init__.py for importlib.resources support")

    def _contains_optional_directory(self, directory: Path) -> bool:
        """Check if directory contains an 'optional' subdirectory"""
        return any(f.name == "optional" and f.is_dir() for f in directory.rglob("*"))

    def _copy_directory_excluding_optional(self, src_dir: Path, dst_dir: Path) -> None:
        """
        Recursively copies a directory, excluding any sub-directory named 'optional'.
        """
        dst_dir.mkdir(parents=True, exist_ok=True)
        for item in src_dir.iterdir():
            if item.name == "optional" and item.is_dir():
                continue

            src_path = src_dir / item.name
            dst_path = dst_dir / item.name

            if item.is_dir():
                self._copy_directory_excluding_optional(src_path, dst_path)
            else:
                shutil.copy2(src_path, dst_path)

    def _relative_to_cwd(self, path: Path) -> Path:
        try:
            return path.relative_to(Path.cwd())
        except ValueError:
            return path  # Not a subpath of CWD, return original
