from pathlib import Path
from typing import Any
from rapidfuzz import fuzz, process  # type: ignore

from .parser import AssetReference
from .sources.base import ResolutionResult, ResolvedSource
from .sources.huggingface import HuggingFaceSource
from .sources.civitai import CivitaiSource
from .config import get_model_path
from . import storage


class Resolver:
    """Resolves asset references to their sources."""
    
    def __init__(self, config: dict[str, Any]):
        """
        Initialize the resolver.
        
        Args:
            config: Configuration dictionary
        """
        self.config = config
        
        # Initialize sources
        hf_token = config.get("api_keys", {}).get("huggingface")
        self.hf_source = HuggingFaceSource(token=hf_token)
        
        civitai_token = config.get("api_keys", {}).get("civitai")
        self.civitai_source = CivitaiSource(token=civitai_token)
    
    def resolve_all(
        self,
        assets: list[AssetReference],
        on_progress: Any | None = None,
    ) -> list[ResolutionResult]:
        """
        Resolve all assets.
        
        Args:
            assets: List of asset references to resolve
            
        Returns:
            List of resolution results
        """
        results = []
        for asset in assets:
            if on_progress:
                on_progress(asset.raw_name)
            result = self.resolve_one(asset)
            result.asset_type = asset.asset_type
            results.append(result)
        return results
    
    def resolve_one(self, asset: AssetReference) -> ResolutionResult:
        # 1. Local
        result = self._check_local(asset)
        if result and result.confidence == "high":
            return result
        # 2. Cache
        result = self._check_cache(asset)
        if result and result.confidence in ("high", "medium"):
            return result
        # 3. Config mappings
        result = self._check_config_mappings(asset)
        if result and result.confidence in ("high", "medium"):
            return result
        # 4. HuggingFace
        result = self._check_huggingface(asset)
        if result and result.confidence in ("high", "medium"):
            return result
        # 5. Civitai — high/medium
        civitai_low: ResolutionResult | None = None
        if self.civitai_source.is_available(self.config):
            results = self.civitai_source.search(asset.raw_name, asset.asset_type)
            for r in results:
                if r.confidence in ("high", "medium"):
                    return r
                if r.confidence == "low" and civitai_low is None:
                    civitai_low = r   # save best low-confidence hit

        # 6. Heuristic
        result = self._heuristic_match(asset)
        if result:
            return result

        # 7. Fall back to best low-confidence Civitai hit rather than UNRESOLVED
        if civitai_low is not None:
            return civitai_low

        return ResolutionResult(
            input_name=asset.raw_name,
            resolved_name=asset.raw_name,
            source=None,
            confidence="unknown",
            error="No resolution found",
        )

    def _check_local(self, asset: AssetReference) -> ResolutionResult | None:
        """
        Check if the asset exists locally in the model directory.
        
        Args:
            asset: Asset reference
            
        Returns:
            Resolution result if found, None otherwise
        """
        try:
            model_dir = get_model_path(asset.asset_type, self.config)
            
            if not model_dir.exists():
                return None
            
            # Check for exact match first
            exact_path = model_dir / asset.raw_name
            if exact_path.exists() and exact_path.is_file():
                source = ResolvedSource(
                    source_type="local",
                    repo=str(model_dir),
                    file=asset.raw_name,
                    download_url=None,
                    sha256=None,
                    file_size_bytes=exact_path.stat().st_size,
                )
                return ResolutionResult(
                    input_name=asset.raw_name,
                    resolved_name=asset.raw_name,
                    source=source,
                    confidence="high",
                    error=None,
                )
            
            # Try fuzzy match
            match_result = self._local_fuzzy_match(asset.raw_name, model_dir)
            if match_result:
                matched_path, score = match_result
                
                # Only return if score is high enough
                if score >= 85:
                    confidence = "high" if score >= 95 else "medium"
                    source = ResolvedSource(
                        source_type="local",
                        repo=str(model_dir),
                        file=matched_path.name,
                        download_url=None,
                        sha256=None,
                        file_size_bytes=matched_path.stat().st_size,
                    )
                    return ResolutionResult(
                        input_name=asset.raw_name,
                        resolved_name=matched_path.name,
                        source=source,
                        confidence=confidence,
                        error=None,
                    )
        
        except Exception:
            pass
        
        return None
    
    def _check_cache(self, asset: AssetReference) -> ResolutionResult | None:
        """
        Check if the asset has a cached mapping.
        
        Args:
            asset: Asset reference
            
        Returns:
            Resolution result if found, None otherwise
        """
        mapping = storage.get_mapping(asset.raw_name)
        if not mapping:
            return None
        
        # Reconstruct ResolutionResult from mapping
        source = None
        if "source" in mapping:
            source_data = mapping["source"]
            source = ResolvedSource(
                source_type=source_data.get("type", "unknown"),
                repo=source_data.get("repo", ""),
                file=source_data.get("file", ""),
                download_url=source_data.get("download_url"),
                sha256=source_data.get("sha256"),
                file_size_bytes=source_data.get("file_size_bytes"),
            )
        
        return ResolutionResult(
            input_name=asset.raw_name,
            resolved_name=mapping.get("resolved_name", asset.raw_name),
            source=source,
            confidence=mapping.get("confidence", "unknown"),
            error=None,
        )
    
    def _check_config_mappings(self, asset: AssetReference) -> ResolutionResult | None:
        """
        Check if the asset has a static mapping in the config.
        
        Args:
            asset: Asset reference
            
        Returns:
            Resolution result if found, None otherwise
        """
        mappings = self.config.get("mappings", {})
        if asset.raw_name not in mappings:
            return None
        
        mapping = mappings[asset.raw_name]
        
        # Handle direct_url type
        if mapping.get("type") == "direct_url":
            url = mapping.get("url")
            if url:
                source = ResolvedSource(
                    source_type="direct_url",
                    repo=url,
                    file=asset.raw_name,
                    download_url=url,
                    sha256=mapping.get("sha256"),
                    file_size_bytes=None,
                )
                return ResolutionResult(
                    input_name=asset.raw_name,
                    resolved_name=asset.raw_name,
                    source=source,
                    confidence="high",
                    error=None,
                )
        
        return None
    
    def _check_huggingface(self, asset: AssetReference) -> ResolutionResult | None:
        """
        Search HuggingFace for the asset.
        
        Args:
            asset: Asset reference
            
        Returns:
            Best resolution result if found, None otherwise
        """
        results = self.hf_source.search(asset.raw_name, asset.asset_type)
        
        # Return the first result with high or medium confidence
        for result in results:
            if result.confidence in ("high", "medium"):
                return result
        
        return None
    
    def _check_civitai(self, asset: AssetReference) -> ResolutionResult | None:
        """
        Search Civitai for the asset.
        
        Args:
            asset: Asset reference
            
        Returns:
            Best resolution result if found, None otherwise
        """
        results = self.civitai_source.search(asset.raw_name, asset.asset_type)
        
        # Return the first result with high or medium confidence
        for result in results:
            if result.confidence in ("high", "medium"):
                return result
        
        return None
    
    def _heuristic_match(self, asset: AssetReference) -> ResolutionResult | None:
        """
        Try to match against a list of known popular model filenames.
        
        Args:
            asset: Asset reference
            
        Returns:
            Low confidence result if a match is found, None otherwise
        """
        # List of known popular model filenames
        known_models = [
            "v1-5-pruned-emaonly.safetensors",
            "sd_xl_base_1.0.safetensors",
            "sd_xl_refiner_1.0.safetensors",
            "flux1-dev.safetensors",
            "flux1-schnell.safetensors",
            "clip_l.safetensors",
            "clip_g.safetensors",
            "t5xxl_fp16.safetensors",
        ]
        
        # Try to find a match
        result = process.extractOne(
            asset.raw_name.lower(),
            [m.lower() for m in known_models],
            scorer=fuzz.ratio
        )
        
        if result and result[1] >= 80:  # Score threshold
            matched_name = known_models[[m.lower() for m in known_models].index(result[0])]
            
            return ResolutionResult(
                input_name=asset.raw_name,
                resolved_name=matched_name,
                source=None,
                confidence="low",
                error="Heuristic match - manual verification recommended",
            )
        
        return None
    
    def _local_fuzzy_match(self, filename: str, directory: Path) -> tuple[Path, float] | None:
        """
        Find the closest matching file in a directory.
        
        Args:
            filename: Filename to match
            directory: Directory to search
            
        Returns:
            Tuple of (matched_path, score) if found, None otherwise
        """
        if not directory.exists():
            return None
        
        # Get all files in directory
        try:
            files = [f for f in directory.iterdir() if f.is_file()]
        except (PermissionError, OSError):
            return None
        
        if not files:
            return None
        
        # Find best match
        file_names = [f.name for f in files]
        result = process.extractOne(
            filename.lower(),
            [n.lower() for n in file_names],
            scorer=fuzz.ratio
        )
        
        if result and result[1] >= 70:  # Minimum score threshold
            matched_index = [n.lower() for n in file_names].index(result[0])
            return files[matched_index], result[1]
        
        return None
