"""Solution module for models."""

from __future__ import annotations

import datetime
import json
import time
from dataclasses import dataclass, field
from functools import cached_property
from typing import TYPE_CHECKING, Any, Final

from pytola.core.logger import get_logger

from .project import Project

if TYPE_CHECKING:
    from pathlib import Path

# 默认排除的目录模式
_DEFAULT_EXCLUDE_PATTERNS: Final[frozenset[str]] = frozenset(
    (
        "dist",
        ".cbuild",
        "build",
        "__pycache__",
        ".git",
        ".pytest_cache",
        ".benchmarks",
        "node_modules",
        ".idea",
    )
)

logger = get_logger(__name__)

# Required attributes for project validation (module-level constant for performance)
_REQUIRED_ATTRS: Final[frozenset[str]] = frozenset(("name", "version", "description"))


@dataclass(frozen=True)
class Solution:
    """Represents a collection of Python projects found in a directory tree.

    This class manages multiple Project instances discovered by scanning a directory
    for pyproject.toml files. It provides methods to load projects from various sources
    (TOML files, JSON files, or directory scans) and handles project data persistence
    by saving/loading to/from projects.json files.

    Attributes
    ----------
        root_dir: The root directory where project scanning originated
        projects: Dictionary mapping project names to their Project instances
        start_time: Float representing the start time for performance measurement
        time_stamp: Datetime object representing when the solution was created
    """

    root_dir: Path
    projects: dict[str, Project]
    start_time: float = field(default_factory=time.perf_counter)
    time_stamp: datetime.datetime = field(default_factory=datetime.datetime.now)

    def __repr__(self) -> str:
        """Return a string representation of the Solution instance."""
        return (
            f"<Solution(\n"
            f"  root_dir={self.root_dir!r},\n"
            f"  projects: {len(self.projects)},\n"
            f"  time_used={self.elapsed_time:.4f}s,\n"
            f"  timestamp={self.time_stamp!r}\n"
            f")>"
        )

    def __post_init__(self):
        """Initialize the Solution instance and log project details."""
        logger.info(
            f"Solution: {len(self.projects)} projects from {self.root_dir}, "
            f"created in {self.elapsed_time:.4f}s at {self.time_stamp:%Y-%m-%d %H:%M:%S}",
        )
        self._write_project_json()

    @cached_property
    def elapsed_time(self) -> float:
        """Calculate and cache the elapsed time since start."""
        return time.perf_counter() - self.start_time

    @cached_property
    def dependencies(self) -> set[str]:
        """Get a set of all dependencies for all projects in the solution.

        Returns
        -------
            Set of dependency package names.
        """
        # Use set comprehension for better performance
        return {dep for project in self.projects.values() for dep in project.dependencies}

    def get_projects_info(self) -> list[dict]:
        """Get detailed information for all projects in the solution.

        Returns
        -------
            List of dictionaries containing detailed project information
        """
        return [project.get_project_info() for project in self.projects.values()]

    @cached_property
    def json_file(self) -> Path:
        """Path to the cache file where project data is stored.

        Returns
        -------
            Path to projects.json file in the root directory.
        """
        return self.root_dir / "projects.json"

    def find_matching_projects(self, pattern: str) -> list[str]:
        """Find all projects matching the given pattern (case-insensitive).

        Args:
            pattern: Pattern to match (substring, case-insensitive)

        Returns
        -------
            List of matching project names
        """
        if not pattern:
            return []

        lower_pattern = pattern.lower()
        return [name for name in self.projects if lower_pattern in name.lower()]

    def resolve_project_name(self, project_name: str | None) -> str | None:
        """Resolve project name with fuzzy matching support.

        Resolution strategy:
        1. If project_name is None: auto-select if only one project exists
        2. Exact match: return if project name exists
        3. Fuzzy match: find projects containing the given substring (case-insensitive)
        4. If multiple fuzzy matches: return None (ambiguous)

        Args:
            project_name: Project name to resolve, or None for auto-selection

        Returns
        -------
            Resolved project name, or None if resolution fails

        Examples
        --------
            >>> solution.resolve_project_name(None)  # Auto-select if single project
            'myproject'
            >>> solution.resolve_project_name("docscan")  # Exact match
            'docscan'
            >>> solution.resolve_project_name("doc")  # Fuzzy match
            'docscan'
            >>> solution.resolve_project_name("scan")  # Fuzzy match
            'docscan'
        """
        # Auto-select if only one project
        if not project_name:
            if len(self.projects) == 1:
                return next(iter(self.projects.keys()))
            return None

        # Exact match (case-sensitive)
        if project_name in self.projects:
            return project_name

        # Fuzzy match (case-insensitive substring matching)
        lower_name = project_name.lower()
        matches = [name for name in self.projects if lower_name in name.lower()]

        if len(matches) == 1:
            return matches[0]
        if len(matches) > 1:
            # Multiple matches found - ambiguous
            return None

        # No match found
        return None

    @classmethod
    def from_toml_files(cls, root_dir: Path, toml_files: list[Path]) -> Solution:
        """Create a Solution instance by parsing multiple pyproject.toml files.

        Args:
            root_dir: Root directory where the projects are located
            toml_files: List of paths to pyproject.toml files to parse

        Returns
        -------
            A Solution instance containing all parsed projects.
        """
        projects: dict[str, Project] = {}
        for toml_file in toml_files:
            if not toml_file.is_file():
                logger.warning(f"Warning: {toml_file} is not a file")
                continue
            project = Project.from_toml_file(toml_file)
            if not project.name:
                logger.warning(
                    f"Warning: {toml_file} does not contain project information",
                )
                continue

            # For multi-project solutions, set solution_root_dir
            # Create new Project with solution_root_dir set
            if len(toml_files) > 1:
                project = Project(
                    name=project.name,
                    version=project.version,
                    description=project.description,
                    readme=project.readme,
                    requires_python=project.requires_python,
                    dependencies=project.dependencies,
                    optional_dependencies=project.optional_dependencies,
                    scripts=project.scripts,
                    entry_points=project.entry_points,
                    authors=project.authors,
                    license=project.license,
                    keywords=project.keywords,
                    classifiers=project.classifiers,
                    urls=project.urls,
                    build_backend=project.build_backend,
                    requires=project.requires,
                    toml_path=project.toml_path,
                    solution_root_dir=root_dir,
                )

            projects[project.name] = project

        return cls(root_dir=root_dir, projects=projects)

    @classmethod
    def from_json_data(cls, root_dir: Path, json_data: dict[str, Any]) -> Solution:
        """Create a Solution instance from JSON data.

        Args:
            root_dir: Root directory for the project collection
            json_data: Dictionary containing project data in JSON format

        Returns
        -------
            A Solution instance containing projects parsed from the JSON data.
        """
        projects = {}

        try:
            for key, value in json_data.items():
                if isinstance(value, dict):
                    # Check if the value has a "project" section (from pyproject.toml parsing)
                    if "project" in value:
                        project_data = value.get("project", {})
                        projects[key] = Project._from_dict(project_data)
                    # Check if the value contains direct project attributes
                    # Use set intersection with module-level constant for faster attribute checking
                    elif value.keys() & _REQUIRED_ATTRS:
                        projects[key] = Project._from_dict(value)
                    else:
                        # No project section or recognizable project attributes found
                        logger.warning(f"No project information found in {key}")
                        projects[key] = Project._from_empty_dict()
                else:
                    projects[key] = value
        except (TypeError, ValueError) as e:
            logger.error(f"Error loading project data from JSON data: {e}")
        except Exception as e:
            logger.error(f"Unknown error loading project data from JSON data: {e}")

        return cls(root_dir=root_dir, projects=projects)

    @classmethod
    def from_json_file(cls, json_file: Path) -> Solution:
        """Create a Solution instance from a JSON file.

        Args:
            json_file: Path to the JSON file containing project data
            update: If True, forces re-parsing even if cache exists

        Returns
        -------
            A Solution instance containing projects parsed from the JSON file.
        """
        if not json_file.is_file():
            logger.error(f"Error: {json_file} is not a file")
            return cls(root_dir=json_file.parent, projects={})

        try:
            logger.debug(f"Loading project data from {json_file}...")
            with json_file.open("r", encoding="utf-8") as f:
                loaded_data = json.load(f)
            logger.debug(f"\t - Loaded project data from {json_file}")
            return cls.from_json_data(json_file.parent, loaded_data)
        except (OSError, json.JSONDecodeError, KeyError) as e:
            logger.error(f"Error loading project data from {json_file}: {e}")
            return cls(root_dir=json_file.parent, projects={})
        except Exception as e:
            logger.error(f"Unknown error loading project data from {json_file}: {e}")
            return cls(root_dir=json_file.parent, projects={})

    @classmethod
    def from_directory(
        cls,
        root_dir: Path,
        exclude_patterns: set[str] | None = None,
    ) -> Solution:
        """Create a Solution instance by scanning a directory for pyproject.toml files.

        This method recursively searches the given directory for pyproject.toml files,
        parses each one, and creates Project instances from them. If a projects.json
        cache file exists and update is False, it will load from the cache instead.

        Args:
            root_dir: Directory to scan for pyproject.toml files
            exclude_patterns: Set of directory names to exclude from search
            update: If True, forces re-parsing even if cache exists

        Returns
        -------
            A Solution instance containing all discovered projects.
        """
        if not root_dir.is_dir():
            logger.error(f"Error: {root_dir} is not a directory")
            return cls(root_dir=root_dir, projects={})

        # Use default exclude patterns if none provided
        if exclude_patterns is None:
            exclude_patterns = _DEFAULT_EXCLUDE_PATTERNS

        logger.debug(
            f"Parsing pyproject.toml in {root_dir} (excluding: {exclude_patterns})...",
        )

        # Find pyproject.toml files with exclusion
        toml_files = []
        for pyproject_file in root_dir.rglob("pyproject.toml"):
            # Check if any parent directory should be excluded
            should_exclude = False
            for parent in pyproject_file.parents:
                if parent.name in exclude_patterns:
                    should_exclude = True
                    break
                # Stop when we reach the root directory
                if parent == root_dir:
                    break

            if not should_exclude:
                toml_files.append(pyproject_file)

        logger.debug(f"Found {len(toml_files)} pyproject.toml files after exclusion")
        return cls.from_toml_files(root_dir, toml_files)

    def _write_project_json(self) -> None:
        """Write the project data to a projects.json file for caching.

        This method serializes the project data to JSON format and saves it
        to a cache file named projects.json in the root directory. This enables
        faster loading on subsequent runs unless the update flag is set.

        Args:
            update: If True, forces writing even if the file already exists
        """
        # Cache json_file reference to avoid repeated cached_property access
        json_file = self.json_file
        try:
            # Pre-cache raw_data access to avoid repeated property access
            serializable_data = {
                key: project_data.raw_data if isinstance(project_data, Project) else project_data
                for key, project_data in self.projects.items()
            }

            with json_file.open("w", encoding="utf-8") as f:
                json.dump(serializable_data, f, indent=2, ensure_ascii=False)
            logger.info(f"Output written to {json_file}")
        except (OSError, json.JSONDecodeError, KeyError) as e:
            logger.error(f"Error writing output to {json_file}: {e}")
        except Exception as e:
            logger.error(f"Unknown error writing output to {json_file}: {e}")
