"""Project module for models."""

from __future__ import annotations

import platform
import re
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from re import Pattern
from typing import Any, Final

import tomli as tomllib

from pytola.core.logger import get_logger

from .dependency import Dependency
from .entryfile import EntryFile

logger = get_logger(__name__)

IS_WINDOWS = platform.system() == "Windows"
_DEP_NAME_PATTERN: Final[Pattern[str]] = re.compile(r"^([a-zA-Z0-9._-]+)")
_QT_DEPENDENCIES: Final[frozenset[str]] = frozenset(
    (
        "Qt",
        "PySide",
        "PyQt",
        "PySide2",
        "PySide6",
        "PyQt5",
        "PyQt6",
        "Qt5",
        "Qt6",
    )
)
_GUI_KEYWORDS: Final[frozenset[str]] = frozenset(("gui", "desktop"))

# 已知GUI项目白名单
_KNOWN_GUI_PROJECTS: Final[frozenset[str]] = frozenset(
    (
        "llmserver",  # LLM服务器GUI
        "lscopt",  # LSC优化器GUI
        "docscan",  # 文档扫描GUI
        "quizbase",  # 测验基础GUI
        "alarmclock",  # 闹钟GUI
        "lscopt_gui",  # LSC优化器GUI版本
    )
)


def is_entry_file(file_path: Path) -> bool:
    """Check if a Python file is an entry file.

    An entry file must contain either:
    - def main() function
    - if __name__ == '__main__': block

    Args:
        file_path: Path to Python file

    Returns
    -------
        True if file is an entry file, False otherwise
    """
    if not file_path.is_file() or file_path.suffix != ".py":
        return False

    try:
        content = file_path.read_text(encoding="utf-8")
        return (
            "def main(" in content or "if __name__ == '__main__':" in content or 'if __name__ == "__main__":' in content
        )
    except Exception:
        return False


def detect_entry_files(project_dir: Path, project_name: str, project_root: Path | None = None) -> list[EntryFile]:
    """Detect all entry files in a project directory.

    Args:
        project_dir: Directory to scan for entry files
        project_name: Name of the project
        project_root: Root directory of the project (for relative path calculations)

    Returns
    -------
        List of EntryFile objects for all detected entry files
    """
    entry_files: list[EntryFile] = []

    # Normalize project name for consistency (hyphens to underscores, lowercase)
    normalized_project_name = project_name.lower().replace("-", "_")

    # Scan all Python files in project directory
    for py_file in project_dir.glob("*.py"):
        if is_entry_file(py_file):
            entry_file = EntryFile(
                project_name=normalized_project_name,
                source_file=py_file,
                project_root=project_root,
            )
            entry_files.append(entry_file)
            logger.debug(f"Found entry file: {entry_file}")

    return entry_files


@dataclass(frozen=True)
class Project:
    """Represents a single Python project parsed from a pyproject.toml file.

    This class encapsulates all metadata and configuration information from a Python project's
    pyproject.toml file. It provides convenient access to project properties and utility methods
    for analyzing project characteristics such as dependencies and GUI frameworks used.

    Attributes
    ----------
        name: The name of the Python package
        version: The version string of the package
        description: Brief description of the package
        readme: Path to the project's README file
        requires_python: Python version requirement (e.g., ">=3.8")
        dependencies: List of required dependencies
        optional_dependencies: Dictionary of optional dependency groups
        scripts: Dictionary of console script entry points
        entry_points: Dictionary of other entry points
        authors: List of author information
        license: License information
        keywords: List of keywords/tags for the package
        classifiers: List of trove classifiers
        urls: Dictionary of project URLs (homepage, repository, etc.)
        build_backend: Build backend system used (e.g., "setuptools.build_meta")
        requires: List of build system requirements
        toml_path: Path to the pyproject.toml file
        solution_root_dir: Root directory of the solution (for multi-project setups)
    """

    name: str
    version: str
    description: str
    readme: str
    requires_python: str
    dependencies: list[str]
    optional_dependencies: dict[str, list[str]]
    scripts: dict[str, str]
    entry_points: dict[str, Any]
    authors: list[dict[str, Any]]
    license: str
    keywords: list[str]
    classifiers: list[str]
    urls: dict[str, str]
    build_backend: str
    requires: list[str]
    toml_path: Path
    solution_root_dir: Path | None = None

    @cached_property
    def min_python_version(self) -> str | None:
        """Extract the minimum Python version from requires_python.

        Parses the requires_python string to extract the minimum version requirement.
        Supports formats like ">=3.8", ">3.7", "~=3.9", etc. Returns None if no
        minimum version is specified or if the format is unrecognized.

        Returns
        -------
            The minimum Python version as a string, or None if not found.
        """
        if not self.requires_python:
            return None

        # Pattern to match minimum Python version requirements
        # Examples: >=3.8, >3.7, ~=3.9, ==3.8.*, etc.
        patterns = [
            r">=(\d+\.\d+(?:\.\d+)?)",  # >=3.8, >=3.8.1
            r">(\d+\.\d+(?:\.\d+)?)",  # >3.7
            r"~=?(\d+\.\d+(?:\.\d+)?)",  # ~=3.9, ~3.9
            r"==?(\d+\.\d+(?:\.\d+)?)(?:\.\*)?",  # ==3.8, ==3.8.*, =3.8
        ]

        for pattern in patterns:
            match = re.search(pattern, self.requires_python)
            if match:
                return match.group(1)

        return None

    @cached_property
    def root_dir(self) -> Path:
        """Return the root directory of the project."""
        if not self.toml_path.is_file():
            # Return current directory if toml_path is not set or invalid
            # This can happen when Project is created with default values
            return Path()

        return self.toml_path.parent

    @cached_property
    def dist_dir(self) -> Path:
        """Return the distribution directory of the project.

        In multi-project solutions, returns solution_root_dir/dist.
        In single-project setups, returns project_root_dir/dist.
        """
        if self.solution_root_dir:
            dist_dir = self.solution_root_dir / "dist"
        else:
            dist_dir = self.root_dir / "dist"
        dist_dir.mkdir(parents=True, exist_ok=True)
        return dist_dir

    @cached_property
    def extension(self) -> str:
        """Return the executable file extension for the project.

        Returns
        -------
            ".exe" on Windows, empty string otherwise
        """
        return ".exe" if IS_WINDOWS else ""

    @cached_property
    def exe_name(self) -> str:
        """Return the executable name of the project (based on project name).

        Note: The actual executable file may have a different name if generated
        from an entry file with a different stem. Use exe_paths to get all
        possible executable paths.
        """
        return f"{self.name}{self.extension}"

    @cached_property
    def exe_path(self) -> Path:
        """Return the executable path of the project (based on project name).

        Note: This returns a single path based on the project name. If the project
        has entry files with different names, use exe_paths to get all possible
        executable paths.
        """
        return self.dist_dir / f"{self.name}{self.extension}"

    def _is_entry_file(self, file_path: Path) -> bool:
        """Check if a Python file is an entry file.

        Delegates to the shared is_entry_file function.

        Args:
            file_path: Path to Python file

        Returns
        -------
            True if file is an entry file, False otherwise
        """
        return is_entry_file(file_path)

    @cached_property
    def entry_file_stems(self) -> list[str]:
        """Legacy property for backward compatibility.

        Returns entry file stems for compatibility with existing code.
        Prefer using entry_files property for new code.

        Returns
        -------
            List of entry file stems (filenames without extension).
        """
        return [entry.entry_name for entry in self.entry_files]

    @cached_property
    def entry_files(self) -> list[EntryFile]:
        """Detect and return all entry files in the project directory.

        Returns
        -------
            List of EntryFile objects representing all entry files found.
        """
        if not self.root_dir:
            return []

        return detect_entry_files(
            project_dir=self.root_dir,
            project_name=self.name,
            project_root=self.solution_root_dir or self.root_dir,
        )

    @cached_property
    def exe_paths(self) -> list[Path]:
        """Return all possible executable paths for the project.

        This method detects entry files in the project directory and returns
        paths for all potential executables. Each entry file (e.g., app.py)
        generates a corresponding executable (e.g., app.exe).

        Returns
        -------
            List of paths to possible executable files.
        """
        # Use the new entry_files property
        entry_names = [entry.entry_name for entry in self.entry_files]

        # If no entry files found, fall back to project name
        if not entry_names:
            return [self.dist_dir / f"{self.name}{self.extension}"]

        return [self.dist_dir / f"{name}{self.extension}" for name in entry_names]

    @cached_property
    def primary_exe(self) -> Path:
        """Return the primary executable path for the project.

        Tries to find an existing executable in the dist directory.
        Falls back to project.exe_path if no executable is found.

        This is useful for installers (like NSIS) that need to create
        shortcuts to the main executable.

        Returns
        -------
            Primary executable path (e.g., Path("dist/docscan.exe"))
        """
        # Check which exe files actually exist in dist directory
        for exe_path in self.exe_paths:
            if exe_path.exists():
                return exe_path

        # Fallback to project name-based exe path
        return self.exe_path

    @cached_property
    def primary_exe_name(self) -> str:
        """Return the primary executable filename for the project.

        This is a convenience property that returns the name of primary_exe.

        Returns
        -------
            Primary executable filename (e.g., "docscan.exe")
        """
        return self.primary_exe.name

    @cached_property
    def additional_exes(self) -> list[str]:
        """Return additional executable filenames (excluding primary_exe).

        This property returns a list of other executable filenames that exist
        in the dist directory, excluding the primary_exe.

        Returns
        -------
            List of additional executable filenames
        """
        primary = self.primary_exe_name
        return [exe_path.name for exe_path in self.exe_paths if exe_path.exists() and exe_path.name != primary]

    @cached_property
    def runtime_dir(self) -> Path:
        """Return the runtime directory of the project."""
        runtime_dir = self.dist_dir / "runtime"
        runtime_dir.mkdir(parents=True, exist_ok=True)
        return runtime_dir

    @cached_property
    def lib_dir(self) -> Path:
        """Return the lib directory of the project."""
        lib_dir = self.dist_dir / "site-packages"
        lib_dir.mkdir(parents=True, exist_ok=True)
        return lib_dir

    @cached_property
    def normalized_name(self) -> str:
        """Return the normalized name of the project (name with hyphens replaced by underscores).

        Returns
        -------
            Normalized project name
        """
        return self.name.replace("-", "_")

    @cached_property
    def converted_dependencies(self) -> set[Dependency]:
        """Convert dependency strings to Dependency objects.

        Returns
        -------
            Set of Dependency objects representing the project's dependencies.
        """
        return {Dependency.from_str(dep) for dep in self.dependencies}

    @cached_property
    def dep_names(self) -> set[str]:
        """Extract normalized dependency names from the dependencies list.

        This method parses dependency strings to extract just the package names,
        removing version specifiers, extras, and environment markers.
        Handles complex dependency formats like: package[extra]>=1.0; python_version>="3.8"

        Returns
        -------
            Set of normalized dependency package names.
        """
        return {
            match.group(1) if (match := _DEP_NAME_PATTERN.match(main_part)) else main_part
            for dep in self.dependencies
            if (main_part := dep.split(";")[0].strip())
        }

    @cached_property
    def qt_deps(self) -> set[str]:
        """Cached intersection of dependencies with Qt packages.

        Returns
        -------
            Set of Qt-related dependency names found in the project.
        """
        # Pre-cache the sets to avoid attribute lookup overhead
        # Convert to lowercase for case-insensitive comparison
        dep_names_lower = {name.lower() for name in self.dep_names}
        qt_deps_lower = {name.lower() for name in _QT_DEPENDENCIES}
        matched_deps = dep_names_lower & qt_deps_lower

        # Return original case names from dependencies
        if matched_deps:
            # Map back to original case names
            original_names = set()
            dep_names_map = {name.lower(): name for name in self.dep_names}
            for matched in matched_deps:
                if matched in dep_names_map:
                    original_names.add(dep_names_map[matched])
            return original_names
        return set()

    @cached_property
    def has_qt(self) -> bool:
        """Check if the project has Qt dependencies.

        Returns
        -------
            True if project has Qt dependencies, False otherwise
        """
        return bool(self.qt_deps)

    @cached_property
    def qt_libname_raw(self) -> str | None:
        """Return the raw Qt library name from dependencies (may be lowercase).

        Returns
        -------
            Raw Qt library name as specified in dependencies, or None if no Qt deps
        """
        return next(iter(self.qt_deps), None)

    @cached_property
    def qt_libname(self) -> str | None:
        """Return the correct Qt library directory name (proper case).

        Maps lowercase dependency names to proper directory names:
        - pyside2 -> PySide2
        - pyqt5 -> PyQt5
        - etc.

        Returns
        -------
            Proper Qt library directory name, or None if no Qt deps
        """
        raw_name = self.qt_libname_raw
        if not raw_name:
            return None

        # Qt库名称映射: 将小写的依赖名称映射到正确的目录名称
        qt_libname_mapping = {
            "pyside2": "PySide2",
            "pyside6": "PySide6",
            "pyqt5": "PyQt5",
            "pyqt6": "PyQt6",
            "qt5": "Qt5",
            "qt6": "Qt6",
        }

        return qt_libname_mapping.get(raw_name.lower(), raw_name)

    @cached_property
    def is_gui(self) -> bool:
        """Determine if the project is a GUI application.

        Returns
        -------
            True if project is a GUI application, False otherwise
        """
        # Use set intersection for O(1) lookup instead of O(n) with any()
        # Also consider projects with Qt dependencies as GUI applications
        return bool(set(self.keywords) & _GUI_KEYWORDS) or self.has_qt

    @cached_property
    def loader_type(self) -> str:
        """Determine the loader type based on project characteristics.

        Returns
        -------
            'gui' if GUI application, 'console' if CLI application, 'console' as default
        """
        return "gui" if self.is_gui else "console"

    @cached_property
    def has_tests(self) -> bool:
        """Check if the project has a tests directory.

        Returns
        -------
            True if tests directory exists and is a directory, False otherwise
        """
        if not self.root_dir:
            return False
        tests_dir = self.root_dir / "tests"
        return tests_dir.exists() and tests_dir.is_dir()

    @cached_property
    def has_benchmarks(self) -> bool:
        """Check if the project has a benchmarks directory.

        Looks for benchmarks in tests/benchmarks or benchmarks/ directories.

        Returns
        -------
            True if benchmarks directory exists and is a directory, False otherwise
        """
        if not self.root_dir:
            return False
        # Check common benchmark locations
        benchmark_locations = [
            self.root_dir / "tests" / "benchmarks",
            self.root_dir / "benchmarks",
        ]
        return any(location.exists() and location.is_dir() for location in benchmark_locations)

    @cached_property
    def project_type(self) -> str:
        """Determine the project type based purely on technical characteristics.

        Uses technical analysis to distinguish between:
        - Window applications (GUI): Have GUI frameworks and GUI entry points
        - Console applications (CLI): Have main entry points and CLI characteristics
        - Libraries/Modules: Neither GUI nor CLI specific

        Returns
        -------
            Project type classification string
        """
        # Check for GUI applications using technical indicators
        if self._is_gui_application():
            return "窗口应用"

        # Check for console applications using technical indicators
        if self._is_console_application():
            return "控制台应用"

        # Default to library/module
        return "库/模块"

    @cached_property
    def project_type_emoji(self) -> str:
        """Get emoji representation of project type.

        Returns
        -------
            Emoji corresponding to project type
        """
        type_mapping = {
            "窗口应用": "🖥️",
            "控制台应用": "⌨️",
            "库/模块": "📚",
            "传统项目": "🔧",
        }
        return type_mapping.get(self.project_type, "❓")

    def _is_gui_application(self) -> bool:
        """Determine if project is a GUI application based on technical characteristics.

        A GUI application should have:
        1. GUI framework dependencies (PySide2, PyQt5, tkinter, etc.)
        2. GUI entry points (gui-scripts)
        3. GUI-related code patterns

        Returns
        -------
            True if project is a GUI application, False otherwise
        """
        # Check for GUI frameworks in dependencies (case-insensitive)
        gui_frameworks_lower = {"pyside2", "pyqt5", "pyside6", "pyqt6", "tkinter", "wx"}
        dep_names_lower = {name.lower() for name in self.dep_names}
        if dep_names_lower & gui_frameworks_lower:
            return True

        # Check for GUI entry points
        if "gui-scripts" in self.entry_points:
            return True

        # Check for GUI-related keywords in project metadata
        gui_keywords = {"gui", "desktop", "window", "界面", "图形界面"}
        if set(self.keywords) & gui_keywords:
            return True

        # Check for main module with GUI framework imports
        main_module = self.root_dir / f"{self.normalized_name}.py"
        if main_module.exists():
            try:
                with open(main_module, encoding="utf-8") as f:
                    content = f.read()

                # Check for GUI framework imports in main module
                gui_imports = [
                    "import PySide2",
                    "from PySide2",
                    "import PyQt5",
                    "from PyQt5",
                    "import PySide6",
                    "from PySide6",
                    "import PyQt6",
                    "from PyQt6",
                    "import tkinter",
                    "from tkinter",
                    "import wx",
                    "from wx",
                ]
                if any(gui_import in content for gui_import in gui_imports):
                    return True

                # Check for Qt application patterns
                qt_patterns = ["QApplication", "QMainWindow", "QWidget"]
                if any(pattern in content for pattern in qt_patterns):
                    return True

            except Exception:
                pass

        return False

    def _is_console_application(self) -> bool:
        """Determine if project is a console application based on technical characteristics.

        A console application should have:
        1. Main entry point (__main__ guard)
        2. CLI framework usage or argument parsing
        3. Console script entry points
        4. Not be a GUI application

        Returns
        -------
            True if project is a console application, False otherwise
        """
        # Early exit if it's a GUI application
        if self._is_gui_application():
            return False

        # Check for console script entry points
        if self.scripts or "console-scripts" in self.entry_points:
            return True

        # Check for main module file
        main_module = self.root_dir / f"{self.normalized_name}.py"
        if not main_module.exists():
            return False

        try:
            with open(main_module, encoding="utf-8") as f:
                content = f.read()

            # Check for CLI framework imports
            cli_frameworks = ["argparse", "click", "typer", "fire", "docopt"]
            has_cli_framework = any(
                f"import {framework}" in content or f"from {framework}" in content for framework in cli_frameworks
            )

            # Check for main function patterns
            has_main_entry = "def main(" in content or "main()" in content

            # Check for argument parsing
            has_arg_parsing = (
                "ArgumentParser" in content
                or ".command(" in content  # click/typer
                or "@app.command" in content  # typer
                or "sys.argv" in content
            )

            # Check for execution patterns
            # Pattern 1: Standard __main__ guard
            has_main_guard = 'if __name__ == "__main__"' in content

            # Pattern 2: Direct main() call at end of file
            lines = content.splitlines()
            has_direct_main_call = len(lines) > 0 and (
                lines[-1].strip() == "main()"
                or any(
                    "main()" in line and not line.strip().startswith("#") for line in lines[-5:]
                )  # Check last 5 lines
            )

            # A console application should have:
            # 1. CLI framework AND main function, OR
            # 2. Main guard OR direct main call, AND argument parsing
            cli_indicators = has_cli_framework and has_main_entry
            execution_indicators = (has_main_guard or has_direct_main_call) and has_arg_parsing

            return cli_indicators or execution_indicators

        except Exception:
            return False

    def is_cli_tool(self) -> bool:
        """Enhanced CLI tool detection with comprehensive heuristics.

        This method provides more sophisticated CLI tool detection than
        the basic _is_console_application method.

        Returns
        -------
            True if project is identified as a CLI tool, False otherwise
        """
        # Use existing console application detection as base
        if self._is_console_application():
            return True

        # Additional heuristic checks
        project_name = self.name
        project_path = self.root_dir

        # Check for common CLI tool files/directories
        cli_indicators = [
            project_path / "cli.py",
            project_path / "main.py",
            project_path / "__main__.py",
            project_path / "bin",
            project_path / "scripts",
            project_path / "commands",
            project_path / (project_name.replace("-", "_") + ".py"),
        ]

        if any(indicator.exists() for indicator in cli_indicators):
            return True

        # Check for __main__.py with typical CLI structure
        main_file = project_path / "__main__.py"
        project_main_file = project_path / (project_name.replace("-", "_") + ".py")

        for main_file_path in [main_file, project_main_file]:
            if main_file_path.exists():
                try:
                    content = main_file_path.read_text(encoding="utf-8")
                    # Look for argparse, click, typer, or fire imports
                    cli_imports = ["argparse", "click", "typer", "fire"]
                    if any(f"import {imp}" in content or f"from {imp}" in content for imp in cli_imports):
                        return True
                    # Look for if __name__ == '__main__' pattern
                    if 'if __name__ == "__main__"' in content:
                        return True
                    # Look for main() function call pattern
                    if "main()" in content and "def main" in content:
                        return True
                except Exception:
                    pass

        # Check for setup.py with console_scripts
        setup_file = project_path / "setup.py"
        if setup_file.exists():
            try:
                content = setup_file.read_text(encoding="utf-8")
                if "console_scripts" in content or "entry_points" in content:
                    return True
            except Exception:
                pass

        return False

    def is_gui_app(self) -> bool:
        """Enhanced GUI application detection with comprehensive heuristics.

        This method provides more sophisticated GUI application detection than
        the basic _is_gui_application method.

        Returns
        -------
            True if project is identified as a GUI application, False otherwise
        """
        # Use existing GUI application detection as base
        if self._is_gui_application():
            return True

        # Additional heuristic checks
        project_name = self.name
        project_path = self.root_dir

        # Check for known GUI projects
        if project_name.lower() in _KNOWN_GUI_PROJECTS:
            return True

        # Check for GUI-specific files
        gui_indicators = [
            project_path / (project_name.replace("-", "_") + "_gui.py"),
            project_path / "gui.py",
            project_path / "mainwindow.py",
            project_path / "window.py",
            project_path / "interface.py",
        ]

        if any(indicator.exists() for indicator in gui_indicators):
            return True

        # Check for GUI framework imports in main files
        main_files_to_check = [
            project_path / (project_name.replace("-", "_") + ".py"),
            project_path / "__main__.py",
        ]

        gui_frameworks = ["PySide2", "PyQt5", "tkinter", "wx"]

        for main_file in main_files_to_check:
            if main_file.exists():
                try:
                    content = main_file.read_text(encoding="utf-8")
                    # Check for GUI framework imports
                    if any(framework in content for framework in gui_frameworks):
                        # Also check for typical GUI classes
                        gui_classes = [
                            "QApplication",
                            "QMainWindow",
                            "QWidget",
                            "Tk",
                        ]
                        if any(gui_class in content for gui_class in gui_classes):
                            return True
                except Exception:
                    pass

        return False

    def get_project_info(self) -> dict:
        """Get comprehensive project information for GUI display.

        Returns
        -------
            Dictionary containing detailed project information
        """
        return {
            "name": self.name,
            "path": str(self.root_dir),
            "type": self.project_type,
            "type_emoji": self.project_type_emoji,
            "description": self.description,
            "version": self.version,
            "python_version": self.requires_python,
            "dependencies": sorted(self.dep_names),
            "entry_points": list(self.scripts.keys()),
            "gui_scripts": list(self.entry_points.get("gui-scripts", {}).keys()),
            "has_qt": self.has_qt,
            "qt_deps": list(self.qt_deps),
            "keywords": self.keywords,
            "file_count": len(list(self.root_dir.glob("*.py"))) if self.root_dir.exists() else 0,
        }

    def __repr__(self) -> str:
        """Return a string representation of the Project instance."""
        return f"<Project {self.name} v{self.version} py{self.requires_python} deps{self.dependencies}>"

    @classmethod
    def from_toml_file(cls, toml_file: Path) -> Project:
        """Create a Project instance by parsing a pyproject.toml file.

        Args:
            toml_file: Path to the pyproject.toml file to parse

        Returns
        -------
            A Project instance containing the parsed project data.
            Returns an empty Project if the file doesn't exist or is invalid.
        """
        if not toml_file.is_file():
            logger.error(f"{toml_file} does not exist")
            return Project._from_empty_dict()

        try:
            with open(toml_file, "rb") as f:
                data = tomllib.load(f)
        except Exception as e:
            logger.error(f"Error parsing {toml_file}: {e}")
            return Project._from_empty_dict()

        if "project" not in data:
            logger.error(f"No project section in {toml_file}")
            return Project._from_empty_dict()

        if "name" not in data["project"]:
            logger.error(f"No name in project section of {toml_file}")
            return Project._from_empty_dict()

        # Extract project data and build system data separately for performance
        project_data = data["project"]
        build_system_data = data.get("build-system", {})

        # Merge data efficiently
        merged_data = project_data.copy()  # Start with project data

        # Handle TOML field names that use hyphens but Python attributes use underscores
        if "requires-python" in merged_data:
            merged_data["requires_python"] = merged_data.pop("requires-python")

        # Handle build-system field names
        if "build-backend" in build_system_data:
            merged_data["build_backend"] = build_system_data["build-backend"]
        if "requires" in build_system_data:
            merged_data["requires"] = build_system_data["requires"]

        merged_data.update({"toml_path": toml_file})

        return Project._from_dict(merged_data)

    def pack_source(self):
        """Pack source code and resources to dist/src directory."""
        pass

    def pack_embed_python(self):
        """Pack Python runtime to dist/runtime directory."""
        pass

    @cached_property
    def raw_data(self) -> dict[str, Any]:
        """Convert the Project instance to a dictionary for JSON serialization.

        Returns
        -------
            Dictionary representation of the Project instance suitable for JSON serialization.
        """
        return {
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "readme": self.readme,
            "requires_python": self.requires_python,
            "dependencies": self.dependencies,
            "optional_dependencies": self.optional_dependencies,
            "scripts": self.scripts,
            "entry_points": self.entry_points,
            "authors": self.authors,
            "license": self.license,
            "keywords": self.keywords,
            "classifiers": self.classifiers,
            "urls": self.urls,
            # build-system data
            "build_backend": self.build_backend,
            "requires": self.requires,
        }

    @classmethod
    def _from_empty_dict(cls) -> Project:
        """Create a Project instance from an empty dictionary."""
        return cls._from_dict({})

    @classmethod
    def _from_dict(cls, data: dict[str, Any]) -> Project:
        """Create a Project instance from a dictionary of project attributes.

        Args:
            data: Dictionary containing project attributes

        Returns
        -------
            A Project instance initialized with the provided data.
        """
        return cls(
            name=data.get("name", ""),
            version=data.get("version", ""),
            description=data.get("description", ""),
            readme=data.get("readme", ""),
            requires_python=data.get("requires_python", ""),
            dependencies=data.get("dependencies", []),
            optional_dependencies=data.get("optional_dependencies", {}),
            scripts=data.get("scripts", {}),
            entry_points=data.get("entry_points", {}),
            authors=data.get("authors", []),
            license=data.get("license", ""),
            keywords=data.get("keywords", []),
            classifiers=data.get("classifiers", []),
            urls=data.get("urls", {}),
            build_backend=data.get("build_backend", ""),
            requires=data.get("requires", []),
            toml_path=data.get("toml_path", Path("")),
            solution_root_dir=data.get("solution_root_dir"),
        )
