"""Image deck loader for matching Tarot card images to cards.

This module provides intelligent image matching and loading, supporting:
- Numbered format: 0.jpg, 1.jpg, ... or 00_foolish.jpg, 01_magic_man.jpg
- Custom naming with override: ##_custom_name.jpg overrides default card names
- Intelligent fuzzy matching for card identification
- Hybrid modes with intelligent fallbacks

Usage:
    from tarot.card.image_loader import load_deck_images

    deck = Deck()
    count = load_deck_images(deck, "/path/to/deck/folder")
    print(f"Loaded {count} card images")
"""

import re
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple

if TYPE_CHECKING:
    from tarot.deck import Card, Deck


class ImageDeckLoader:
    """Loader for matching Tarot card images to deck cards."""

    # Supported image extensions
    SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}

    # Regex patterns for file matching
    NUMBERED_PATTERN = re.compile(
        r"^(\d+)(?:_(.+))?\.(?:jpg|jpeg|png|gif|bmp|webp)$", re.IGNORECASE
    )

    def __init__(self, deck_folder: str) -> None:
        """
        Initialize the image deck loader.

        Args:
            deck_folder: Path to the folder containing card images

        Raises:
            ValueError: If folder doesn't exist or is not a directory
        """
        self.deck_folder = Path(deck_folder)

        if not self.deck_folder.exists():
            raise ValueError(f"Deck folder does not exist: {deck_folder}")

        if not self.deck_folder.is_dir():
            raise ValueError(f"Deck path is not a directory: {deck_folder}")

        self.image_files = self._scan_folder()
        self.card_mapping: Dict[int, Tuple[str, bool]] = (
            {}
        )  # card_number -> (path, has_custom_name)
        self._build_mapping()

    def _scan_folder(self) -> List[Path]:
        """Scan folder for image files."""
        images = []
        for ext in self.SUPPORTED_EXTENSIONS:
            images.extend(self.deck_folder.glob(f"*{ext}"))
            images.extend(self.deck_folder.glob(f"*{ext.upper()}"))

        # Sort by filename for consistent ordering
        return sorted(images)

    def _parse_filename(self, filename: str) -> Tuple[Optional[int], Optional[str], bool]:
        """
        Parse image filename to extract card number and optional custom name.

        Args:
            filename: The filename (without path)

        Returns:
            Tuple of (card_number, custom_name, has_custom_name)
            - card_number: Parsed number if found, else None
            - custom_name: Custom name if present (e.g., "foolish" from "00_foolish.jpg")
            - has_custom_name: True if custom name was found

        Examples:
            "0.jpg" -> (0, None, False)
            "00_foolish.jpg" -> (0, "foolish", True)
            "01_magic_man.jpg" -> (1, "magic_man", True)
            "invalid.jpg" -> (None, None, False)
        """
        match = self.NUMBERED_PATTERN.match(filename)

        if not match:
            return None, None, False

        card_number = int(match.group(1))
        custom_name = match.group(2)
        has_custom_name = custom_name is not None

        return card_number, custom_name, has_custom_name

    def _build_mapping(self) -> None:
        """Build mapping from card numbers to image file paths."""
        for image_path in self.image_files:
            card_num, custom_name, has_custom_name = self._parse_filename(image_path.name)

            if card_num is not None:
                # Store path and whether it has a custom name
                self.card_mapping[card_num] = (str(image_path), has_custom_name)

    def _normalize_card_name(self, name: str) -> str:
        """
        Normalize card name for matching.

        Converts to lowercase, removes special characters, collapses whitespace.

        Args:
            name: Original card name

        Returns:
            Normalized name

        Examples:
            "The Fool" -> "the fool"
            "Princess of Swords" -> "princess of swords"
            "Ace of Cups" -> "ace of cups"
        """
        # Convert to lowercase
        normalized = name.lower()

        # Replace special characters with spaces
        normalized = re.sub(r"[^\w\s]", " ", normalized)

        # Collapse multiple spaces
        normalized = re.sub(r"\s+", " ", normalized).strip()

        return normalized

    def _find_fuzzy_match(self, card_name_normalized: str) -> Optional[int]:
        """
        Find matching card number using fuzzy name matching.

        This is a fallback when card names don't parse as numbers.

        Args:
            card_name_normalized: Normalized card name

        Returns:
            Card number if a match is found, else None
        """
        best_match = None
        best_score = 0
        threshold = 0.6

        # Check all parsed custom names
        for card_num, (_, has_custom_name) in self.card_mapping.items():
            if not has_custom_name:
                continue

            # Get the actual filename to extract custom name
            for image_path in self.image_files:
                parsed_num, custom_name, _ = self._parse_filename(image_path.name)

                if parsed_num == card_num and custom_name:
                    normalized_custom = self._normalize_card_name(custom_name)

                    # Simple similarity score: words that match
                    query_words = set(card_name_normalized.split())
                    custom_words = set(normalized_custom.split())

                    if query_words and custom_words:
                        intersection = len(query_words & custom_words)
                        union = len(query_words | custom_words)
                        score = intersection / union if union > 0 else 0

                        if score > best_score and score >= threshold:
                            best_score = score
                            best_match = card_num

        return best_match

    def get_image_path(self, card: "Card") -> Optional[str]:
        """
        Get the image path for a specific card.

        Matches cards by:
        1. Card number (primary method)
        2. Fuzzy matching on card name (fallback)

        Args:
            card: The Card object to find an image for

        Returns:
            Full path to image file, or None if not found
        """
        # Try direct number match first
        if card.number in self.card_mapping:
            path, _ = self.card_mapping[card.number]
            return path

        # Try fuzzy match on name as fallback
        normalized_name = self._normalize_card_name(card.name)
        fuzzy_match = self._find_fuzzy_match(normalized_name)

        if fuzzy_match is not None and fuzzy_match in self.card_mapping:
            path, _ = self.card_mapping[fuzzy_match]
            return path

        return None

    def should_override_name(self, card_number: int) -> bool:
        """
        Check if card name should be overridden from filename.

        Returns True only if:
        - Image file has a custom name component (##_name.jpg format)
        - Not just a plain number (##.jpg format)

        Args:
            card_number: The card's number

        Returns:
            True if name should be overridden from filename, False otherwise
        """
        if card_number not in self.card_mapping:
            return False

        _, has_custom_name = self.card_mapping[card_number]
        return has_custom_name

    def get_custom_name(self, card_number: int) -> Optional[str]:
        """
        Get the custom card name from the filename.

        Args:
            card_number: The card's number

        Returns:
            Custom name if present, None otherwise

        Example:
            If filename is "00_the_foolish.jpg", returns "the_foolish"
            If filename is "00.jpg", returns None
        """
        if card_number not in self.card_mapping:
            return None

        # Find the image file for this card number
        for image_path in self.image_files:
            _, custom_name, _ = self._parse_filename(image_path.name)

            parsed_num, _, _ = self._parse_filename(image_path.name)
            if parsed_num == card_number and custom_name:
                # Convert underscore-separated name to title case
                name_words = custom_name.split("_")
                return " ".join(word.capitalize() for word in name_words)

        return None

    def load_into_deck(
        self, deck: "Deck", override_names: bool = True, verbose: bool = False
    ) -> int:
        """
        Load image paths into all cards in a deck.

        Args:
            deck: The Deck to load images into
            override_names: If True, use custom names from filenames when available
            verbose: If True, print progress information

        Returns:
            Number of cards that had images loaded

        Example:
            >>> loader = ImageDeckLoader("/path/to/deck")
            >>> deck = Deck()
            >>> count = loader.load_into_deck(deck, override_names=True)
            >>> print(f"Loaded {count} card images")
        """
        loaded_count = 0

        for card in deck.cards:
            image_path = self.get_image_path(card)

            if image_path:
                card.image_path = image_path
                loaded_count += 1

                # Override name if appropriate
                if override_names and self.should_override_name(card.number):
                    custom_name = self.get_custom_name(card.number)
                    if custom_name:
                        if verbose:
                            print(f"  {card.number}: {card.name} -> {custom_name}")
                        card.name = custom_name
                elif verbose:
                    print(f"  ✓ {card.number}: {card.name}")

        return loaded_count

    def get_summary(self) -> Dict[str, any]:
        """
        Get a summary of loaded images and statistics.

        Returns:
            Dictionary with loader statistics
        """
        total_images = len(self.image_files)
        mapped_cards = len(self.card_mapping)
        custom_named = sum(1 for _, has_custom in self.card_mapping.values() if has_custom)

        return {
            "deck_folder": str(self.deck_folder),
            "total_image_files": total_images,
            "total_image_filenames": len(set(f.name for f in self.image_files)),
            "mapped_card_numbers": mapped_cards,
            "cards_with_custom_names": custom_named,
            "cards_with_generic_numbers": mapped_cards - custom_named,
            "image_extensions_found": list(set(f.suffix.lower() for f in self.image_files)),
        }


def load_deck_images(
    deck: "Deck", deck_folder: str, override_names: bool = True, verbose: bool = False
) -> int:
    """
    Convenience function to load deck images.

    Args:
        deck: The Deck object to load images into
        deck_folder: Path to folder containing card images
        override_names: If True, use custom names from filenames when available
        verbose: If True, print progress information

    Returns:
        Number of cards that had images loaded

    Raises:
        ValueError: If deck_folder doesn't exist or is invalid

    Example:
        >>> from tarot import Deck
        >>> from tarot.card.image_loader import load_deck_images
        >>>
        >>> deck = Deck()
        >>> count = load_deck_images(deck, "/path/to/deck/images")
        >>> print(f"Loaded {count} card images")
    """
    loader = ImageDeckLoader(deck_folder)
    return loader.load_into_deck(deck, override_names=override_names, verbose=verbose)
