"""
Version detection for COPY_IF_MISSING files.

This module provides functionality to detect and compare versions in synced files,
enabling users to be notified when their customized files are outdated compared
to the template source.

Version detection works by parsing version comments at the top of files:
- YAML: # persuade-version: 1.0.0
- Markdown: <!-- persuade-version: 1.0.0 -->
- Shell: # persuade-version: 1.0.0
- Python: # persuade-version: 1.0.0
"""

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Final

from persuade.logging import get_logger

logger = get_logger()

# Version header patterns for different file types
# Format: persuade-version: X.Y.Z (semantic versioning)
VERSION_PATTERNS: Final[dict[str, re.Pattern[str]]] = {
    # YAML, Shell, Python, generic hash-comment files
    "hash_comment": re.compile(r"#\s*persuade-version:\s*(\d+\.\d+\.\d+)"),
    # Markdown, HTML (XML-style comments)
    "xml_comment": re.compile(r"<!--\s*persuade-version:\s*(\d+\.\d+\.\d+)\s*-->"),
    # JSON (using a special key)
    "json_key": re.compile(r'"_persuade_version"\s*:\s*"(\d+\.\d+\.\d+)"'),
}

# Map file extensions to pattern types
EXTENSION_TO_PATTERN: Final[dict[str, str]] = {
    ".yml": "hash_comment",
    ".yaml": "hash_comment",
    ".sh": "hash_comment",
    ".py": "hash_comment",
    ".md": "xml_comment",
    ".json": "json_key",
    ".txt": "hash_comment",
    # Files without extension (like Justfile, .bandit)
    "": "hash_comment",
}


@dataclass
class VersionInfo:
    """Version information for a file."""

    version: str | None
    """The semantic version string (e.g., '1.0.0') or None if not found."""

    file_path: Path
    """Path to the file."""

    def __post_init__(self) -> None:
        """Validate version format if present."""
        if self.version is not None and not re.match(r"^\d+\.\d+\.\d+$", self.version):
            raise ValueError(f"Invalid version format: {self.version}")


@dataclass
class VersionComparison:
    """Result of comparing source and target versions."""

    source_version: str | None
    target_version: str | None
    is_outdated: bool
    """True if target version is older than source version."""
    source_path: Path
    target_path: Path

    @property
    def has_versions(self) -> bool:
        """Check if both files have version information."""
        return self.source_version is not None and self.target_version is not None

    @property
    def source_has_version(self) -> bool:
        """Check if source file has version information."""
        return self.source_version is not None


def parse_version(version_str: str) -> tuple[int, int, int]:
    """Parse semantic version string into tuple of integers.

    Args:
        version_str: Version string in format X.Y.Z

    Returns:
        Tuple of (major, minor, patch)

    Raises:
        ValueError: If version string is not in valid format
    """
    parts = version_str.split(".")
    if len(parts) != 3:
        raise ValueError(f"Invalid version format: {version_str}")
    return (int(parts[0]), int(parts[1]), int(parts[2]))


def compare_versions(source_version: str, target_version: str) -> int:
    """Compare two semantic version strings.

    Args:
        source_version: Source version string (X.Y.Z)
        target_version: Target version string (X.Y.Z)

    Returns:
        -1 if source < target
        0 if source == target
        1 if source > target
    """
    source = parse_version(source_version)
    target = parse_version(target_version)

    if source < target:
        return -1
    elif source > target:
        return 1
    return 0


def extract_version(file_path: Path) -> VersionInfo:
    """Extract version information from a file.

    Reads the first 50 lines of the file looking for a version header.
    This is efficient and handles most files with version headers at the top.

    Args:
        file_path: Path to the file to check

    Returns:
        VersionInfo with version string if found, None otherwise
    """
    if not file_path.exists():
        return VersionInfo(version=None, file_path=file_path)

    # Determine which pattern to use based on file extension
    extension = file_path.suffix.lower()

    # Handle files without extension (like Justfile, .bandit)
    if not extension:
        extension = ""  # Use default hash_comment pattern

    pattern_type = EXTENSION_TO_PATTERN.get(extension, "hash_comment")
    pattern = VERSION_PATTERNS[pattern_type]

    try:
        with open(file_path, encoding="utf-8") as f:
            # Read first 50 lines (version should be near the top)
            for _ in range(50):
                line = f.readline()
                if not line:
                    break

                match = pattern.search(line)
                if match:
                    version = match.group(1)
                    logger.debug(
                        "version_found",
                        file=str(file_path),
                        version=version,
                    )
                    return VersionInfo(version=version, file_path=file_path)

    except (OSError, UnicodeDecodeError) as e:
        logger.debug(
            "version_extraction_failed",
            file=str(file_path),
            error=str(e),
        )

    return VersionInfo(version=None, file_path=file_path)


def compare_file_versions(source_path: Path, target_path: Path) -> VersionComparison:
    """Compare versions between source and target files.

    Args:
        source_path: Path to source (template) file
        target_path: Path to target (project) file

    Returns:
        VersionComparison with comparison results
    """
    source_info = extract_version(source_path)
    target_info = extract_version(target_path)

    # Determine if target is outdated
    is_outdated = False

    if source_info.version and target_info.version:
        # Both have versions - compare them
        comparison = compare_versions(source_info.version, target_info.version)
        is_outdated = comparison > 0  # source is newer
    elif source_info.version and not target_info.version:
        # Source has version, target doesn't - consider outdated
        # This handles the case where version was added to source after target was created
        is_outdated = True

    return VersionComparison(
        source_version=source_info.version,
        target_version=target_info.version,
        is_outdated=is_outdated,
        source_path=source_path,
        target_path=target_path,
    )


def check_outdated_files(
    source_path: Path,
    target_path: Path,
    files_to_check: list[str],
) -> list[VersionComparison]:
    """Check multiple files for outdated versions.

    Args:
        source_path: Root path of source repository
        target_path: Root path of target project
        files_to_check: List of relative file paths to check

    Returns:
        List of VersionComparison objects for outdated files only
    """
    outdated: list[VersionComparison] = []

    for rel_path in files_to_check:
        source_file = source_path / rel_path
        target_file = target_path / rel_path

        if not target_file.exists():
            continue  # Skip files that don't exist in target

        comparison = compare_file_versions(source_file, target_file)

        if comparison.is_outdated:
            outdated.append(comparison)
            logger.info(
                "outdated_file_detected",
                file=rel_path,
                source_version=comparison.source_version,
                target_version=comparison.target_version,
            )

    return outdated


def format_version_warning(comparisons: list[VersionComparison]) -> str:
    """Format a warning message for outdated files.

    Args:
        comparisons: List of VersionComparison objects for outdated files

    Returns:
        Formatted warning message string
    """
    if not comparisons:
        return ""

    lines = [
        "⚠️  The following COPY_IF_MISSING files have newer versions available:",
        "",
    ]

    for comp in comparisons:
        # Use relative path for clearer display when files have same name in different dirs
        try:
            rel_path = comp.target_path.relative_to(Path.cwd())
        except ValueError:
            rel_path = comp.target_path
        if comp.target_version:
            lines.append(f"  • {rel_path}: {comp.target_version} → {comp.source_version}")
        else:
            lines.append(f"  • {rel_path}: (no version) → {comp.source_version}")

    lines.extend(
        [
            "",
            "To update these files, use: persuade sync --force-update",
            "Or manually update individual files while preserving your customizations.",
        ]
    )

    return "\n".join(lines)


def add_version_header(file_path: Path, version: str) -> str:
    """Generate a version header for a file based on its type.

    Args:
        file_path: Path to the file (used to determine comment style)
        version: Version string to include

    Returns:
        Version header string appropriate for the file type
    """
    extension = file_path.suffix.lower()

    if extension == ".md":
        return f"<!-- persuade-version: {version} -->\n"
    elif extension == ".json":
        # For JSON, this needs to be added as a key inside the object
        return f'"_persuade_version": "{version}"'
    else:
        # Default: hash-style comment (YAML, Shell, Python, Justfile, .bandit, etc.)
        return f"# persuade-version: {version}\n"
