"""Asset storage with file management.

Metadata is stored in SQLite via BaseSqliteStore; actual files live
in the assets directory on disk.
"""

import mimetypes
import shutil
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import List

from ledgrab.storage.asset import Asset, asset_type_from_mime
from ledgrab.storage.base_sqlite_store import BaseSqliteStore
from ledgrab.storage.database import Database
from ledgrab.utils import get_logger

logger = get_logger(__name__)


# Per-asset-type extension allow-list. Keys must match values produced by
# `asset_type_from_mime` ("sound", "image", "video", "other").
_ALLOWED_EXTENSIONS: dict[str, frozenset[str]] = {
    "image": frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}),
    "video": frozenset({".mp4", ".webm", ".mov", ".mkv", ".avi"}),
    "sound": frozenset({".wav", ".mp3", ".ogg", ".flac", ".m4a", ".aac"}),
}


# Server-authoritative MIME map (fallback when extension is unknown).
_EXT_TO_MIME: dict[str, str] = {
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".webp": "image/webp",
    ".bmp": "image/bmp",
    ".mp4": "video/mp4",
    ".webm": "video/webm",
    ".mov": "video/quicktime",
    ".mkv": "video/x-matroska",
    ".avi": "video/x-msvideo",
    ".wav": "audio/wav",
    ".mp3": "audio/mpeg",
    ".ogg": "audio/ogg",
    ".flac": "audio/flac",
    ".m4a": "audio/mp4",
    ".aac": "audio/aac",
}


class UnsupportedAssetTypeError(ValueError):
    """Raised when an upload's extension is not in the allow-list."""


class AssetStore(BaseSqliteStore[Asset]):
    """Persistent storage for uploaded file assets."""

    _table_name = "assets"
    _entity_name = "Asset"

    def __init__(self, db: Database, assets_dir: str | Path):
        self._assets_dir = Path(assets_dir)
        self._assets_dir.mkdir(parents=True, exist_ok=True)
        super().__init__(db, Asset.from_dict)

    # -- Aliases for consistency with other stores ----------------------------

    get_all_assets = BaseSqliteStore.get_all
    get_asset = BaseSqliteStore.get

    def get_visible_assets(self) -> List[Asset]:
        """Return all assets that are not soft-deleted."""
        return [a for a in self._items.values() if not a.deleted]

    def get_assets_by_type(self, asset_type: str) -> List[Asset]:
        """Return visible assets filtered by type."""
        return [a for a in self._items.values() if not a.deleted and a.asset_type == asset_type]

    def get_file_path(self, asset_id: str) -> Path | None:
        """Resolve the on-disk path for an asset's file. Returns None if missing."""
        asset = self._items.get(asset_id)
        if asset is None or asset.deleted:
            return None
        path = self._assets_dir / asset.stored_filename
        return path if path.exists() else None

    def create_asset(
        self,
        name: str,
        filename: str,
        file_data: bytes,
        mime_type: str | None = None,
        description: str | None = None,
        tags: List[str] | None = None,
        prebuilt: bool = False,
        icon: str | None = None,
        icon_color: str | None = None,
    ) -> Asset:
        """Create a new asset from uploaded file data.

        Args:
            name: Display name for the asset.
            filename: Original upload filename.
            file_data: Raw file bytes.
            mime_type: MIME type (auto-detected from filename if not provided).
            description: Optional description.
            tags: Optional tags.
            prebuilt: Whether this is a shipped prebuilt asset.

        Returns:
            The created Asset.
        """
        self._check_name_unique(name)

        # Server-authoritative type derivation: ignore client mime, derive
        # from extension. Fall back to mimetypes only if we don't have a
        # mapping in our explicit table.
        ext = Path(filename).suffix.lower()
        derived_mime = _EXT_TO_MIME.get(ext)
        if derived_mime:
            mime_type = derived_mime
        else:
            mime_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"

        asset_type = asset_type_from_mime(mime_type)

        # Allow-list extensions per type. Reject anything we don't know.
        allowed = _ALLOWED_EXTENSIONS.get(asset_type)
        if allowed is None or ext not in allowed:
            raise UnsupportedAssetTypeError(
                f"File extension {ext!r} is not allowed for asset_type "
                f"{asset_type!r}. Allowed: "
                f"{sorted(allowed) if allowed else 'none for this type'}."
            )

        # Generate unique stored filename
        stored_filename = f"{uuid.uuid4().hex}{ext}"

        # Write file to disk
        dest = self._assets_dir / stored_filename
        dest.write_bytes(file_data)

        asset_id = f"asset_{uuid.uuid4().hex[:8]}"
        now = datetime.now(timezone.utc)

        asset = Asset(
            id=asset_id,
            name=name,
            filename=filename,
            stored_filename=stored_filename,
            mime_type=mime_type,
            asset_type=asset_type,
            size_bytes=len(file_data),
            created_at=now,
            updated_at=now,
            description=description,
            tags=tags or [],
            prebuilt=prebuilt,
            icon=icon or "",
            icon_color=icon_color or "",
        )

        self._items[asset_id] = asset
        self._save_item(asset_id, asset)
        logger.info(
            f"Created asset: {name} ({asset_id}, type={asset_type}, {len(file_data)} bytes)"
        )
        return asset

    def update_asset(
        self,
        asset_id: str,
        name: str | None = None,
        description: str | None = None,
        tags: List[str] | None = None,
        icon: str | None = None,
        icon_color: str | None = None,
    ) -> Asset:
        """Update asset metadata (not the file itself)."""
        asset = self.get(asset_id)

        if name is not None:
            self._check_name_unique(name, exclude_id=asset_id)
            asset.name = name
        if description is not None:
            asset.description = description
        if tags is not None:
            asset.tags = tags
        if icon is not None:
            asset.icon = icon or ""
        if icon_color is not None:
            asset.icon_color = icon_color or ""

        asset.updated_at = datetime.now(timezone.utc)
        self._save_item(asset_id, asset)
        logger.info(f"Updated asset: {asset_id}")
        return asset

    def delete_asset(self, asset_id: str) -> None:
        """Delete an asset. Prebuilt assets are soft-deleted; others are fully removed."""
        asset = self.get(asset_id)

        if asset.prebuilt:
            # Soft-delete: mark as deleted, remove file but keep metadata
            asset.deleted = True
            asset.updated_at = datetime.now(timezone.utc)
            self._save_item(asset_id, asset)
            file_path = self._assets_dir / asset.stored_filename
            if file_path.exists():
                file_path.unlink()
            logger.info(f"Soft-deleted prebuilt asset: {asset_id}")
        else:
            # Hard delete: remove file and metadata
            file_path = self._assets_dir / asset.stored_filename
            if file_path.exists():
                file_path.unlink()
            del self._items[asset_id]
            self._delete_item(asset_id)
            logger.info(f"Deleted asset: {asset_id}")

    def restore_prebuilt(self, prebuilt_dir: Path) -> List[Asset]:
        """Re-import any soft-deleted prebuilt assets from the prebuilt directory.

        Returns list of restored assets.
        """
        restored = []
        for asset in list(self._items.values()):
            if asset.prebuilt and asset.deleted:
                # Find original file in prebuilt dir
                src = prebuilt_dir / asset.filename
                if src.exists():
                    dest = self._assets_dir / asset.stored_filename
                    shutil.copy2(src, dest)
                    asset.deleted = False
                    asset.size_bytes = dest.stat().st_size
                    asset.updated_at = datetime.now(timezone.utc)
                    self._save_item(asset.id, asset)
                    restored.append(asset)
                    logger.info(f"Restored prebuilt asset: {asset.name} ({asset.id})")
        return restored

    def heal_prebuilt_files(self, prebuilt_dir: Path) -> List[Asset]:
        """Re-copy shipped files for prebuilt assets whose on-disk file is missing.

        Covers the case where the database row still exists (``prebuilt=True``,
        ``deleted=False``) but ``stored_filename`` has been lost — e.g. after a
        partial restore from backup, a data-dir move, or manual file deletion.
        Contrast with :meth:`restore_prebuilt`, which only handles the
        user-initiated soft-delete flow.

        Returns list of healed assets.
        """
        if not prebuilt_dir.exists():
            return []

        healed: List[Asset] = []
        for asset in self._items.values():
            if not asset.prebuilt or asset.deleted:
                continue
            dest = self._assets_dir / asset.stored_filename
            if dest.exists():
                continue
            src = prebuilt_dir / asset.filename
            if not src.exists():
                logger.warning(
                    "Prebuilt asset %r (%s) is missing on disk and its source "
                    "%s is also missing — cannot heal.",
                    asset.name,
                    asset.id,
                    src,
                )
                continue
            shutil.copy2(src, dest)
            asset.size_bytes = dest.stat().st_size
            asset.updated_at = datetime.now(timezone.utc)
            self._save_item(asset.id, asset)
            healed.append(asset)
            logger.warning(
                "Healed missing prebuilt asset %r (%s) -> %s",
                asset.name,
                asset.id,
                asset.stored_filename,
            )
        return healed

    def warn_missing_custom_files(self) -> List[Asset]:
        """Log a warning for each non-prebuilt asset whose file is missing.

        Custom files cannot be auto-healed (there's no authoritative source),
        but surfacing them on startup makes the state of the data visible
        rather than only appearing as 404s when the UI tries to play them.

        Returns the list of assets with missing files.
        """
        missing: List[Asset] = []
        for asset in self._items.values():
            if asset.prebuilt or asset.deleted:
                continue
            if not (self._assets_dir / asset.stored_filename).exists():
                missing.append(asset)
        if missing:
            logger.warning(
                "%d custom asset file(s) missing on disk — the UI will return "
                "404 when trying to load them. Re-upload or delete the "
                "orphan records: %s",
                len(missing),
                ", ".join(f"{a.name} ({a.id})" for a in missing),
            )
        return missing

    def import_prebuilt_sounds(self, prebuilt_dir: Path) -> List[Asset]:
        """Import prebuilt sound files that don't already exist as assets.

        Called on startup. Also heals prebuilt assets whose metadata row
        exists but whose stored file is missing on disk, and logs a warning
        for any custom assets in the same situation.

        Returns list of newly imported assets (not healed or missing ones).
        """
        if not prebuilt_dir.exists():
            return []

        # Heal first so a freshly-restored DB with missing prebuilt files
        # recovers before the UI has a chance to request them.
        self.heal_prebuilt_files(prebuilt_dir)
        self.warn_missing_custom_files()

        # Build set of known prebuilt filenames (including deleted ones)
        known_filenames = {a.filename for a in self._items.values() if a.prebuilt}

        imported = []
        for src in sorted(prebuilt_dir.iterdir()):
            if not src.is_file():
                continue
            if src.name in known_filenames:
                continue

            file_data = src.read_bytes()
            # Derive a friendly name from filename: "chime.wav" -> "Chime"
            friendly_name = src.stem.replace("_", " ").replace("-", " ").title()

            asset = self.create_asset(
                name=friendly_name,
                filename=src.name,
                file_data=file_data,
                prebuilt=True,
            )
            imported.append(asset)
            logger.info(f"Imported prebuilt sound: {src.name} -> {asset.id}")

        return imported
