"""Parser for Civitai image metadata format."""

import json
import logging
from typing import Dict, Any, Union
from ..base import RecipeMetadataParser
from ..constants import GEN_PARAM_KEYS, VALID_LORA_TYPES
from ...services.metadata_service import get_default_metadata_provider
from ...config import config

logger = logging.getLogger(__name__)


class CivitaiApiMetadataParser(RecipeMetadataParser):
    """Parser for Civitai image metadata format"""

    def is_metadata_matching(self, user_comment) -> bool:
        """Check if the metadata matches the Civitai image metadata format

        Args:
            user_comment: The metadata from the image (dict)

        Returns:
            bool: True if this parser can handle the metadata
        """
        metadata = user_comment
        if not metadata or not isinstance(metadata, dict):
            return False

        def has_markers(payload: Dict[str, Any]) -> bool:
            # Check for common CivitAI image metadata fields
            civitai_image_fields = (
                "resources",
                "civitaiResources",
                "additionalResources",
                "hashes",
                "prompt",
                "negativePrompt",
                "steps",
                "sampler",
                "cfgScale",
                "seed",
                "width",
                "height",
                "Model",
                "Model hash",
                "modelVersionIds",
            )
            return any(key in payload for key in civitai_image_fields)

        # Check the main metadata object
        if has_markers(metadata):
            return True

        # Check for LoRA hash patterns
        hashes = metadata.get("hashes")
        if isinstance(hashes, dict) and any(
            str(key).lower().startswith("lora:") for key in hashes
        ):
            return True

        # Check nested meta object (common in CivitAI image responses)
        nested_meta = metadata.get("meta")
        if isinstance(nested_meta, dict):
            if has_markers(nested_meta):
                return True

            # Also check for LoRA hash patterns in nested meta
            hashes = nested_meta.get("hashes")
            if isinstance(hashes, dict) and any(
                str(key).lower().startswith("lora:") for key in hashes
            ):
                return True

        return False

    async def parse_metadata(  # pyright: ignore[reportIncompatibleMethodOverride]
        self, user_comment, recipe_scanner=None, civitai_client=None,
        local_cache: dict[str, Any] | None = None,
    ) -> Dict[str, Any]:
        """Parse metadata from Civitai image format

        Args:
            user_comment: The metadata from the image (dict)
            recipe_scanner: Optional recipe scanner service
            civitai_client: Optional Civitai API client (deprecated, use metadata_provider instead)
            local_cache: Optional dict mapping sha256/autov3 hash → scanner cache item.
                         When provided, matching models skip CivitAI API calls.

        Returns:
            Dict containing parsed recipe data
        """
        metadata: Dict[str, Any] = user_comment
        try:
            # Get metadata provider instead of using civitai_client directly
            metadata_provider = await get_default_metadata_provider()

            # Civitai image responses may wrap the actual metadata inside a "meta" key
            if (
                isinstance(metadata, dict)
                and "meta" in metadata
                and isinstance(metadata["meta"], dict)
            ):
                inner_meta = metadata["meta"]
                if any(
                    key in inner_meta
                    for key in (
                        "resources",
                        "civitaiResources",
                        "additionalResources",
                        "hashes",
                        "prompt",
                        "negativePrompt",
                    )
                ):
                    metadata = inner_meta

            # Civitai's image API meta parser mangles the A1111 "Lora hashes"
            # text field into a quote-wrapped dict entry:
            #   '"Daphne Blake Cosplay_v1": "e67ebd5e315f"'
            # The 12-char AutoV3 it carries is more reliable than the stale
            # 10-char AutoV2 value in the "hashes" dict, so recover it and
            # let it override the conflicting entry.
            if isinstance(metadata, dict):
                for key, hash_value in list(metadata.items()):
                    if (
                        isinstance(key, str)
                        and key.startswith('"')
                        and isinstance(hash_value, str)
                        and hash_value.endswith('"')
                    ):
                        clean_name = key.strip('"').strip()
                        clean_hash = hash_value.strip('"').strip()
                        if clean_name and clean_hash:
                            hashes_dict = metadata.get("hashes")
                            if isinstance(hashes_dict, dict):
                                hashes_dict[f"lora:{clean_name}"] = clean_hash

            # Initialize result structure
            result: Dict[str, Any] = {
                "base_model": None,
                "loras": [],
                "model": None,
                "gen_params": {},
                "from_civitai_image": True,
            }

            # Track already added LoRAs to prevent duplicates
            added_loras: Dict[str, Any] = {}  # key: model_version_id or hash, value: index in result["loras"]

            # Extract hash information from hashes field for LoRA matching
            lora_hashes: Dict[str, Any] = {}
            if "hashes" in metadata and isinstance(metadata["hashes"], dict):
                for key, hash_value in metadata["hashes"].items():
                    key_str = str(key)
                    if key_str.lower().startswith("lora:"):
                        lora_name = key_str.split(":", 1)[1]
                        lora_hashes[lora_name] = hash_value

            # Extract prompt and negative prompt
            if "prompt" in metadata:
                result["gen_params"]["prompt"] = metadata["prompt"]

            if "negativePrompt" in metadata:
                result["gen_params"]["negative_prompt"] = metadata["negativePrompt"]

            # Extract other generation parameters
            param_mapping = {
                "steps": "steps",
                "sampler": "sampler",
                "cfgScale": "cfg_scale",
                "seed": "seed",
                "Size": "size",
                "clipSkip": "clip_skip",
            }

            for civitai_key, our_key in param_mapping.items():
                if civitai_key in metadata and our_key in GEN_PARAM_KEYS:
                    result["gen_params"][our_key] = metadata[civitai_key]

            # Extract base model information - directly if available
            if "baseModel" in metadata:
                result["base_model"] = metadata["baseModel"]
            elif "Model hash" in metadata and metadata_provider:
                model_hash = metadata["Model hash"]
                model_info, error = await metadata_provider.get_model_by_hash(
                    model_hash
                )
                if model_info:
                    result["base_model"] = model_info.get("baseModel", "")
            elif "Model" in metadata and isinstance(metadata.get("resources"), list):
                # Try to find base model in resources
                for resource in metadata.get("resources", []):
                    if resource.get("type") == "model" and resource.get(
                        "name"
                    ) == metadata.get("Model"):
                        # This is likely the checkpoint model
                        if metadata_provider and resource.get("hash"):
                            (
                                model_info,
                                error,
                            ) = await metadata_provider.get_model_by_hash(
                                resource.get("hash")
                            )
                            if model_info:
                                result["base_model"] = model_info.get("baseModel", "")

            base_model_counts: Dict[str, int] = {}

            # Process standard resources array
            if "resources" in metadata and isinstance(metadata["resources"], list):
                for resource in metadata["resources"]:
                    resource_type = resource.get("type", "lora")

                    # Track resources with type "model" — these are checkpoint models.
                    # The resources array is the most reliable source for checkpoint
                    # identification because it has an explicit type field and hash,
                    # unlike modelVersionIds which is a flat list with no type info.
                    if resource_type == "model":
                        checkpoint_entry: Dict[str, Any] = {
                            "id": 0,
                            "modelId": 0,
                            "name": resource.get("name", "Unknown Model"),
                            "version": "",
                            "type": resource.get("type", "model"),
                            "existsLocally": False,
                            "localPath": None,
                            "file_name": resource.get("name", ""),
                            "hash": resource.get("hash", "") or "",
                            "thumbnailUrl": "/loras_static/images/no-preview.png",
                            "baseModel": "",
                            "size": 0,
                            "downloadUrl": "",
                            "isDeleted": False,
                        }

                        # Try to look up base model from the checkpoint hash
                        cp_hash = checkpoint_entry.get("hash")
                        if cp_hash and metadata_provider:
                            # local_cache keys are stored lowercase
                            local_cached = local_cache.get(cp_hash.lower()) if local_cache else None
                            if local_cached:
                                self._populate_entry_from_cache(
                                    checkpoint_entry, local_cached
                                )
                                bm = checkpoint_entry.get("baseModel", "")
                                if bm and not result["base_model"]:
                                    result["base_model"] = bm
                            else:
                                try:
                                    civitai_info = (
                                        await metadata_provider.get_model_by_hash(
                                            cp_hash
                                        )
                                    )
                                    civitai_data, error_msg = (
                                        (civitai_info, None)
                                        if not isinstance(civitai_info, tuple)
                                        else civitai_info
                                    )
                                    if civitai_data and error_msg != "Model not found":
                                        if 'model' in civitai_data and 'name' in civitai_data['model']:
                                            checkpoint_entry['name'] = civitai_data['model']['name']
                                        checkpoint_entry['id'] = civitai_data.get('id', 0)
                                        checkpoint_entry['modelId'] = civitai_data.get('modelId', 0)
                                        if 'name' in civitai_data:
                                            checkpoint_entry['version'] = civitai_data['name']
                                        base_model = civitai_data.get('baseModel', '')
                                        if base_model:
                                            checkpoint_entry['baseModel'] = base_model
                                            if not result['base_model']:
                                                result['base_model'] = base_model
                                except Exception as e:
                                    logger.error(
                                        f"Error fetching checkpoint info for hash "
                                        f"{cp_hash}: {e}"
                                    )

                        if result["model"] is None:
                            result["model"] = checkpoint_entry
                        continue

                    # Modified to process resources without a type field as potential LoRAs
                    if resource_type == "lora":
                        lora_hash = resource.get("hash", "")

                        # Try to get hash from the hashes field if not present in resource
                        if not lora_hash and resource.get("name"):
                            lora_hash = lora_hashes.get(resource["name"], "")

                        # Skip LoRAs without proper identification (hash or modelVersionId)
                        if not lora_hash and not resource.get("modelVersionId"):
                            logger.debug(
                                f"Skipping LoRA resource '{resource.get('name', 'Unknown')}' - no hash or modelVersionId"
                            )
                            continue

                        # Skip if we've already added this LoRA by hash
                        if lora_hash and lora_hash in added_loras:
                            continue

                        lora_entry = {
                            "name": resource.get("name", "Unknown LoRA"),
                            "type": "lora",
                            "weight": float(resource.get("weight", 1.0)),
                            "hash": lora_hash,
                            "existsLocally": False,
                            "localPath": None,
                            "file_name": resource.get("name", "Unknown"),
                            "thumbnailUrl": "/loras_static/images/no-preview.png",
                            "baseModel": "",
                            "size": 0,
                            "downloadUrl": "",
                            "isDeleted": False,
                        }

                        # Try to get info from Civitai if hash is available
                        if lora_hash and metadata_provider:
                            # local_cache keys are stored lowercase
                            local_cached = local_cache.get(lora_hash.lower()) if local_cache else None
                            if local_cached:
                                cached_type = self._cache_item_model_type(local_cached)
                                if cached_type and cached_type not in VALID_LORA_TYPES:
                                    logger.debug(
                                        f"Skipping non-LoRA cache item for hash {lora_hash}"
                                    )
                                    continue
                                self._populate_entry_from_cache(
                                    lora_entry, local_cached
                                )
                                # Track by version ID for deduplication
                                if lora_entry.get("id"):
                                    added_loras[str(lora_entry["id"])] = len(
                                        result["loras"]
                                    )
                                # Mirror base.py:150-151 counts for API-path loras
                                bm = local_cached.get("base_model") or ""
                                if bm:
                                    base_model_counts[bm] = base_model_counts.get(
                                        bm, 0
                                    ) + 1
                            else:
                                try:
                                    civitai_info = (
                                        await metadata_provider.get_model_by_hash(lora_hash)
                                    )

                                    populated_entry = await self.populate_lora_from_civitai(
                                        lora_entry,
                                        civitai_info,
                                        recipe_scanner,
                                        base_model_counts,
                                        lora_hash,
                                    )

                                    if populated_entry is None:
                                        continue  # Skip invalid LoRA types

                                    lora_entry = populated_entry

                                    # If we have a version ID from Civitai, track it for deduplication
                                    if "id" in lora_entry and lora_entry["id"]:
                                        added_loras[str(lora_entry["id"])] = len(
                                            result["loras"]
                                        )
                                except Exception as e:
                                    logger.error(
                                        f"Error fetching Civitai info for LoRA hash {lora_entry['hash']}: {e}"
                                    )

                        # Track by hash if we have it
                        if lora_hash:
                            added_loras[lora_hash] = len(result["loras"])

                        result["loras"].append(lora_entry)

            # Process civitaiResources array
            if "civitaiResources" in metadata and isinstance(
                metadata["civitaiResources"], list
            ):
                for resource in metadata["civitaiResources"]:
                    # Get resource type and identifier
                    resource_type = str(resource.get("type") or "").lower()
                    version_id = str(resource.get("modelVersionId", ""))

                    if resource_type == "checkpoint":
                        checkpoint_entry = {
                            "id": resource.get("modelVersionId", 0),
                            "modelId": resource.get("modelId", 0),
                            "name": resource.get("modelName", "Unknown Checkpoint"),
                            "version": resource.get("modelVersionName", ""),
                            "type": resource.get("type", "checkpoint"),
                            "existsLocally": False,
                            "localPath": None,
                            "file_name": resource.get("modelName", ""),
                            "hash": resource.get("hash", "") or "",
                            "thumbnailUrl": "/loras_static/images/no-preview.png",
                            "baseModel": "",
                            "size": 0,
                            "downloadUrl": "",
                            "isDeleted": False,
                        }

                        if version_id and metadata_provider:
                            try:
                                civitai_info = (
                                    await metadata_provider.get_model_version_info(
                                        version_id
                                    )
                                )

                                checkpoint_entry = (
                                    await self.populate_checkpoint_from_civitai(
                                        checkpoint_entry, civitai_info
                                    )
                                )
                            except Exception as e:
                                logger.error(
                                    f"Error fetching Civitai info for checkpoint version {version_id}: {e}"
                                )

                        if result["model"] is None:
                            result["model"] = checkpoint_entry

                        continue

                    # Skip if we've already added this LoRA
                    if version_id and version_id in added_loras:
                        continue

                    # Initialize lora entry
                    lora_entry = {
                        "id": resource.get("modelVersionId", 0),
                        "modelId": resource.get("modelId", 0),
                        "name": resource.get("modelName", "Unknown LoRA"),
                        "version": resource.get("modelVersionName", ""),
                        "type": resource.get("type", "lora"),
                        "weight": round(float(resource.get("weight", 1.0)), 2),
                        "existsLocally": False,
                        "thumbnailUrl": "/loras_static/images/no-preview.png",
                        "baseModel": "",
                        "size": 0,
                        "downloadUrl": "",
                        "isDeleted": False,
                    }

                    # Try to get info from Civitai if modelVersionId is available
                    if version_id and metadata_provider:
                        try:
                            # Use get_model_version_info instead of get_model_version
                            civitai_info = (
                                await metadata_provider.get_model_version_info(
                                    version_id
                                )
                            )

                            populated_entry = await self.populate_lora_from_civitai(
                                lora_entry,
                                civitai_info,
                                recipe_scanner,
                                base_model_counts,
                            )

                            if populated_entry is None:
                                continue  # Skip invalid LoRA types

                            lora_entry = populated_entry
                        except Exception as e:
                            logger.error(
                                f"Error fetching Civitai info for model version {version_id}: {e}"
                            )

                    # Track this LoRA in our deduplication dict
                    if version_id:
                        added_loras[version_id] = len(result["loras"])

                    result["loras"].append(lora_entry)

            # Process additionalResources array
            if "additionalResources" in metadata and isinstance(
                metadata["additionalResources"], list
            ):
                for resource in metadata["additionalResources"]:
                    # Skip resources that aren't LoRAs or LyCORIS
                    if (
                        resource.get("type") not in ["lora", "lycoris"]
                        and "type" not in resource
                    ):
                        continue

                    lora_type = resource.get("type", "lora")
                    name = resource.get("name", "")

                    # Extract ID from URN format if available
                    version_id = None
                    if name and "civitai:" in name:
                        parts = name.split("@")
                        if len(parts) > 1:
                            version_id = parts[1]

                            # Skip if we've already added this LoRA
                            if version_id in added_loras:
                                continue

                    lora_entry = {
                        "name": name,
                        "type": lora_type,
                        "weight": float(resource.get("strength", 1.0)),
                        "hash": "",
                        "existsLocally": False,
                        "localPath": None,
                        "file_name": name,
                        "thumbnailUrl": "/loras_static/images/no-preview.png",
                        "baseModel": "",
                        "size": 0,
                        "downloadUrl": "",
                        "isDeleted": False,
                    }

                    # If we have a version ID and metadata provider, try to get more info
                    if version_id and metadata_provider:
                        try:
                            # Use get_model_version_info with the version ID
                            civitai_info = (
                                await metadata_provider.get_model_version_info(
                                    version_id
                                )
                            )

                            populated_entry = await self.populate_lora_from_civitai(
                                lora_entry,
                                civitai_info,
                                recipe_scanner,
                                base_model_counts,
                            )

                            if populated_entry is None:
                                continue  # Skip invalid LoRA types

                            lora_entry = populated_entry

                            # Track this LoRA for deduplication
                            if version_id:
                                added_loras[version_id] = len(result["loras"])
                        except Exception as e:
                            logger.error(
                                f"Error fetching Civitai info for model ID {version_id}: {e}"
                            )

                        result["loras"].append(lora_entry)

            # Process modelVersionIds from Civitai image API.
            # These are version IDs returned at root level of the API response.
            # When resources or civitaiResources are already present in metadata
            # (which they are when ?withMeta=true is passed), those sections have
            # complete hash/type information — modelVersionIds is a fallback for
            # when meta is null and only the flat ID list is available. Skipping
            # it here avoids duplicates: the same file hash often resolves to
            # different version IDs via hash lookup (resources) vs the original
            # version ID in modelVersionIds, and both paths would create entries.
            if (
                "modelVersionIds" in metadata
                and isinstance(metadata["modelVersionIds"], list)
                and not result.get("loras")
            ):

                for version_id in metadata["modelVersionIds"]:
                    version_id_str = str(version_id)

                    # Skip if we've already added this LoRA by version ID
                    if version_id_str in added_loras:
                        continue

                    # Skip if this version ID is already the recipe's checkpoint
                    # (resolved earlier from embedded resources/Model hash,
                    # avoiding a duplicate CivitAI API call).
                    existing_model = result.get("model")
                    if existing_model and str(existing_model.get("id")) == version_id_str:
                        continue

                    # Initialize lora entry with version ID
                    lora_entry = {
                        "id": version_id,
                        "modelId": 0,
                        "name": "Unknown LoRA",
                        "version": "",
                        "type": "lora",
                        "weight": 1.0,
                        "existsLocally": False,
                        "thumbnailUrl": "/loras_static/images/no-preview.png",
                        "baseModel": "",
                        "size": 0,
                        "downloadUrl": "",
                        "isDeleted": False,
                    }

                    # Fetch model info from Civitai
                    if metadata_provider and version_id_str:
                        try:
                            civitai_info = (
                                await metadata_provider.get_model_version_info(
                                    version_id_str
                                )
                            )

                            populated_entry = await self.populate_lora_from_civitai(
                                lora_entry,
                                civitai_info,
                                recipe_scanner,
                                base_model_counts,
                            )

                            if populated_entry is None:
                                # Not a LoRA — try as checkpoint (only if we
                                # don't already have one).  Reuses the same
                                # civitai_info from the API call above so no
                                # extra query is made.
                                if result["model"] is None:
                                    checkpoint_entry = {
                                        "id": version_id,
                                        "modelId": 0,
                                        "name": "Unknown Model",
                                        "version": "",
                                        "type": "checkpoint",
                                        "existsLocally": False,
                                        "localPath": None,
                                        "file_name": "",
                                        "hash": "",
                                        "thumbnailUrl": (
                                            "/loras_static/images/no-preview.png"
                                        ),
                                        "baseModel": "",
                                        "size": 0,
                                        "downloadUrl": "",
                                        "isDeleted": False,
                                    }
                                    cp_populated = await (
                                        self.populate_checkpoint_from_civitai(
                                            checkpoint_entry, civitai_info
                                        )
                                    )
                                    if cp_populated.get("modelId"):
                                        result["model"] = cp_populated
                                continue  # Not a LoRA, don't add to loras

                            lora_entry = populated_entry

                        except Exception as e:
                            logger.error(
                                f"Error fetching Civitai info for model version {version_id}: {e}"
                            )

                    # Track this LoRA for deduplication
                    if version_id_str:
                        added_loras[version_id_str] = len(result["loras"])

                    result["loras"].append(lora_entry)

            # If we found LoRA hashes in the metadata but haven't already
            # populated entries for them, fall back to creating LoRAs from
            # the hashes section. Some Civitai image responses only include
            # LoRA information here without explicit resources entries.
            for lora_name, lora_hash in lora_hashes.items():
                if not lora_hash:
                    continue

                # Skip LoRAs we've already added via resources or other fields
                if lora_hash in added_loras:
                    continue

                lora_entry = {
                    "name": lora_name,
                    "type": "lora",
                    "weight": 1.0,
                    "hash": lora_hash,
                    "existsLocally": False,
                    "localPath": None,
                    "file_name": lora_name,
                    "thumbnailUrl": "/loras_static/images/no-preview.png",
                    "baseModel": "",
                    "size": 0,
                    "downloadUrl": "",
                    "isDeleted": False,
                }

                if metadata_provider:
                    # local_cache keys are stored lowercase
                    local_cached = local_cache.get(lora_hash.lower()) if local_cache else None
                    if local_cached:
                        cached_type = self._cache_item_model_type(local_cached)
                        if cached_type and cached_type not in VALID_LORA_TYPES:
                            logger.debug(
                                f"Skipping non-LoRA cache item for hash {lora_hash}"
                            )
                            continue
                        self._populate_entry_from_cache(lora_entry, local_cached)
                        # Mirror base.py:150-151 counts for API-path loras
                        bm = local_cached.get("base_model") or ""
                        if bm:
                            base_model_counts[bm] = base_model_counts.get(bm, 0) + 1
                        if "id" in lora_entry and lora_entry["id"]:
                            added_loras[str(lora_entry["id"])] = len(result["loras"])
                    else:
                        try:
                            civitai_info = await metadata_provider.get_model_by_hash(
                                lora_hash
                            )

                            populated_entry = await self.populate_lora_from_civitai(
                                lora_entry,
                                civitai_info,
                                recipe_scanner,
                                base_model_counts,
                                lora_hash,
                            )

                            if populated_entry is None:
                                continue

                            lora_entry = populated_entry

                            if "id" in lora_entry and lora_entry["id"]:
                                added_loras[str(lora_entry["id"])] = len(result["loras"])
                        except Exception as e:
                            logger.error(
                                f"Error fetching Civitai info for LoRA hash {lora_hash}: {e}"
                            )

                added_loras[lora_hash] = len(result["loras"])
                result["loras"].append(lora_entry)

            # Check for LoRA info in the format "Lora_0 Model hash", "Lora_0 Model name", etc.
            lora_index = 0
            while (
                f"Lora_{lora_index} Model hash" in metadata
                and f"Lora_{lora_index} Model name" in metadata
            ):
                lora_hash = metadata[f"Lora_{lora_index} Model hash"]
                lora_name = metadata[f"Lora_{lora_index} Model name"]
                lora_strength_model = float(
                    metadata.get(f"Lora_{lora_index} Strength model", 1.0)
                )

                # Skip if we've already added this LoRA by hash
                if lora_hash and lora_hash in added_loras:
                    lora_index += 1
                    continue

                lora_entry = {
                    "name": lora_name,
                    "type": "lora",
                    "weight": lora_strength_model,
                    "hash": lora_hash,
                    "existsLocally": False,
                    "localPath": None,
                    "file_name": lora_name,
                    "thumbnailUrl": "/loras_static/images/no-preview.png",
                    "baseModel": "",
                    "size": 0,
                    "downloadUrl": "",
                    "isDeleted": False,
                }

                # Try to get info from Civitai if hash is available
                if lora_entry["hash"] and metadata_provider:
                    # local_cache keys are stored lowercase
                    local_cached = local_cache.get(lora_hash.lower()) if local_cache else None
                    if local_cached:
                        cached_type = self._cache_item_model_type(local_cached)
                        if cached_type and cached_type not in VALID_LORA_TYPES:
                            logger.debug(
                                f"Skipping non-LoRA cache item for hash {lora_hash}"
                            )
                            lora_index += 1
                            continue  # Skip non-LoRA cache items
                        self._populate_entry_from_cache(lora_entry, local_cached)
                        # Mirror base.py:150-151 counts for API-path loras
                        bm = local_cached.get("base_model") or ""
                        if bm:
                            base_model_counts[bm] = base_model_counts.get(bm, 0) + 1
                        # If we have a version ID from Civitai, track it for deduplication
                        if "id" in lora_entry and lora_entry["id"]:
                            added_loras[str(lora_entry["id"])] = len(result["loras"])
                    else:
                        try:
                            civitai_info = await metadata_provider.get_model_by_hash(
                                lora_hash
                            )

                            populated_entry = await self.populate_lora_from_civitai(
                                lora_entry,
                                civitai_info,
                                recipe_scanner,
                                base_model_counts,
                                lora_hash,
                            )

                            if populated_entry is None:
                                lora_index += 1
                                continue  # Skip invalid LoRA types

                            lora_entry = populated_entry

                            # If we have a version ID from Civitai, track it for deduplication
                            if "id" in lora_entry and lora_entry["id"]:
                                added_loras[str(lora_entry["id"])] = len(result["loras"])
                        except Exception as e:
                            logger.error(
                                f"Error fetching Civitai info for LoRA hash {lora_entry['hash']}: {e}"
                            )

                # Track by hash if we have it
                if lora_hash:
                    added_loras[lora_hash] = len(result["loras"])

                result["loras"].append(lora_entry)

                lora_index += 1

            # If base model wasn't found earlier, use the most common one from LoRAs
            if not result["base_model"] and base_model_counts:
                result["base_model"] = max(
                    base_model_counts.items(), key=lambda x: x[1]
                )[0]

            return result

        except Exception as e:
            logger.error(f"Error parsing Civitai image metadata: {e}", exc_info=True)
            return {"error": str(e), "loras": []}

    @staticmethod
    def _populate_entry_from_cache(
        entry: dict[str, Any],
        cache_item: dict[str, Any],
    ) -> None:
        """Fill a lora/checkpoint entry from a scanner cache item.

        Avoids CivitAI API calls for models that exist locally.
        Mirrors the population logic in
        ``RecipeMetadataParser.populate_lora_from_civitai()`` but operates
        entirely on cached data.
        """
        civ = cache_item.get("civitai") or {}
        if isinstance(civ, dict):
            if civ.get("id") is not None:
                entry["id"] = civ["id"]
            if civ.get("modelId") is not None:
                entry["modelId"] = civ["modelId"]
            if civ.get("name"):
                entry["version"] = civ["name"]
            cached_name = cache_item.get("model_name")
            if cached_name:
                entry["name"] = cached_name
        entry["existsLocally"] = True
        local_path = cache_item.get("file_path")
        if local_path:
            entry["localPath"] = local_path
        sha256 = cache_item.get("sha256")
        if sha256:
            entry["hash"] = sha256
        if "preview_url" in cache_item:
            entry["thumbnailUrl"] = config.get_preview_static_url(
                cache_item["preview_url"]
            )
        base_model = cache_item.get("base_model", "")
        if base_model:
            entry["baseModel"] = base_model

    @staticmethod
    def _cache_item_model_type(cache_item: dict[str, Any]) -> str:
        """Lowercased civitai.model.type of a cache item, or '' when unknown."""
        civ = cache_item.get("civitai")
        if not isinstance(civ, dict):
            return ""
        model_info = civ.get("model")
        if not isinstance(model_info, dict):
            return ""
        return (model_info.get("type") or "").lower()
