"""Thumbnail generation and caching service."""

import asyncio
import hashlib
import logging
import shutil
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# Thumbnail sizes
THUMBNAIL_SIZES = {
    "small": (150, 150),
    "medium": (300, 300),
}

# Cache size limit (500MB)
CACHE_SIZE_LIMIT = 500 * 1024 * 1024  # 500MB in bytes


class ThumbnailService:
    """Service for generating and caching thumbnails."""

    @staticmethod
    def get_cache_dir() -> Path:
        """Get the thumbnail cache directory path.

        Returns user-writable platform cache dir so installs under
        ``%PROGRAMFILES%`` / ``/opt`` work without elevated permissions.
        Mirrors the platform branching of ``config.get_config_dir``.
        """
        import os

        if os.name == "nt":
            # %LOCALAPPDATA% so the cache survives roaming-profile sync.
            base = Path(os.environ.get("LOCALAPPDATA")
                        or os.environ.get("APPDATA")
                        or Path.home() / "AppData" / "Local")
            cache_dir = base / "media-server" / "cache" / "thumbnails"
        else:
            # XDG_CACHE_HOME convention; falls back to ~/.cache.
            xdg = os.environ.get("XDG_CACHE_HOME")
            base = Path(xdg) if xdg else Path.home() / ".cache"
            cache_dir = base / "media-server" / "thumbnails"

        cache_dir.mkdir(parents=True, exist_ok=True)
        return cache_dir

    @staticmethod
    def get_cache_key(file_path: Path) -> str:
        """Generate cache key from file path.

        Args:
            file_path: Path to the media file.

        Returns:
            SHA256 hash of the absolute file path.
        """
        absolute_path = str(file_path.resolve())
        return hashlib.sha256(absolute_path.encode()).hexdigest()

    # Sentinel value indicating "no thumbnail available" is cached
    NO_THUMBNAIL = b""

    @staticmethod
    def get_cached_thumbnail(file_path: Path, size: str) -> Optional[bytes]:
        """Get cached thumbnail if valid.

        Args:
            file_path: Path to the media file.
            size: Thumbnail size ("small" or "medium").

        Returns:
            Thumbnail bytes if cached, empty bytes if cached as "no thumbnail",
            None if not cached.
        """
        cache_dir = ThumbnailService.get_cache_dir()
        cache_key = ThumbnailService.get_cache_key(file_path)
        cache_folder = cache_dir / cache_key
        cache_path = cache_folder / f"{size}.jpg"
        no_thumb_path = cache_folder / ".no_thumbnail"

        # Check negative cache first (no thumbnail available)
        if no_thumb_path.exists():
            try:
                file_mtime = file_path.stat().st_mtime
                cache_mtime = no_thumb_path.stat().st_mtime
                if file_mtime <= cache_mtime:
                    return ThumbnailService.NO_THUMBNAIL
            except (OSError, PermissionError):
                pass

        if not cache_path.exists():
            return None

        # Check if file has been modified since cache was created
        try:
            file_mtime = file_path.stat().st_mtime
            cache_mtime = cache_path.stat().st_mtime

            if file_mtime > cache_mtime:
                logger.debug(f"Cache invalidated for {file_path.name} (file modified)")
                return None

            # Read cached thumbnail
            with open(cache_path, "rb") as f:
                return f.read()

        except (OSError, PermissionError) as e:
            logger.error(f"Error reading cached thumbnail: {e}")
            return None

    @staticmethod
    def cache_thumbnail(file_path: Path, size: str, image_data: bytes) -> None:
        """Cache a thumbnail.

        Args:
            file_path: Path to the media file.
            size: Thumbnail size ("small" or "medium").
            image_data: Thumbnail image data (JPEG bytes).
        """
        cache_dir = ThumbnailService.get_cache_dir()
        cache_key = ThumbnailService.get_cache_key(file_path)
        cache_folder = cache_dir / cache_key
        cache_folder.mkdir(parents=True, exist_ok=True)

        cache_path = cache_folder / f"{size}.jpg"

        try:
            with open(cache_path, "wb") as f:
                f.write(image_data)
            logger.debug(f"Cached thumbnail for {file_path.name} ({size})")
        except (OSError, PermissionError) as e:
            logger.error(f"Error caching thumbnail: {e}")

    @staticmethod
    def cache_no_thumbnail(file_path: Path) -> None:
        """Cache that a file has no thumbnail (negative cache)."""
        cache_dir = ThumbnailService.get_cache_dir()
        cache_key = ThumbnailService.get_cache_key(file_path)
        cache_folder = cache_dir / cache_key
        cache_folder.mkdir(parents=True, exist_ok=True)

        try:
            (cache_folder / ".no_thumbnail").touch()
            logger.debug(f"Cached no-thumbnail for {file_path.name}")
        except (OSError, PermissionError) as e:
            logger.error(f"Error caching no-thumbnail marker: {e}")

    @staticmethod
    def generate_audio_thumbnail(file_path: Path, size: str) -> Optional[bytes]:
        """Generate thumbnail from audio file (extract album art).

        Args:
            file_path: Path to the audio file.
            size: Thumbnail size ("small" or "medium").

        Returns:
            Thumbnail bytes (JPEG) or None if no album art.
        """
        try:
            from io import BytesIO

            from mutagen import File as MutagenFile
            from PIL import Image

            audio = MutagenFile(str(file_path))
            if audio is None:
                return None

            # Extract album art
            art_data = None

            # Try different tag types for album art
            if hasattr(audio, "pictures") and audio.pictures:
                # FLAC, Ogg Vorbis
                art_data = audio.pictures[0].data
            elif hasattr(audio, "tags"):
                tags = audio.tags
                if tags is not None:
                    # MP3 (ID3)
                    if hasattr(tags, "getall"):
                        apic_frames = tags.getall("APIC")
                        if apic_frames:
                            art_data = apic_frames[0].data
                    # MP4/M4A
                    elif "covr" in tags:
                        art_data = bytes(tags["covr"][0])
                    # Try other common keys
                    elif "APIC:" in tags:
                        art_data = tags["APIC:"].data

            if art_data is None:
                return None

            # Resize image
            img = Image.open(BytesIO(art_data))

            # Convert to RGB if necessary (handle RGBA, grayscale, etc.)
            if img.mode not in ("RGB", "L"):
                img = img.convert("RGB")

            # Resize with maintaining aspect ratio and center crop
            target_size = THUMBNAIL_SIZES[size]
            img.thumbnail((target_size[0] * 2, target_size[1] * 2), Image.Resampling.LANCZOS)

            # Center crop to square
            width, height = img.size
            min_dim = min(width, height)
            left = (width - min_dim) // 2
            top = (height - min_dim) // 2
            right = left + min_dim
            bottom = top + min_dim
            img = img.crop((left, top, right, bottom))

            # Final resize
            img = img.resize(target_size, Image.Resampling.LANCZOS)

            # Save as JPEG
            output = BytesIO()
            img.save(output, format="JPEG", quality=85, optimize=True)
            return output.getvalue()

        except ImportError:
            logger.error("Required libraries (mutagen, Pillow) not installed")
            return None
        except Exception as e:
            logger.debug(f"Error generating audio thumbnail for {file_path.name}: {e}")
            return None

    @staticmethod
    async def generate_video_thumbnail(file_path: Path, size: str) -> Optional[bytes]:
        """Generate thumbnail from video file using ffmpeg.

        Args:
            file_path: Path to the video file.
            size: Thumbnail size ("small" or "medium").

        Returns:
            Thumbnail bytes (JPEG) or None if ffmpeg not available.
        """
        try:
            from io import BytesIO

            from PIL import Image

            # Check if ffmpeg is available
            if not shutil.which("ffmpeg"):
                logger.debug("ffmpeg not available, cannot generate video thumbnail")
                return None

            # Extract frame at 10% duration
            target_size = THUMBNAIL_SIZES[size]

            # Use ffmpeg to extract a frame
            cmd = [
                "ffmpeg",
                "-i", str(file_path),
                "-vf", (
                    f"thumbnail,scale={target_size[0]}:{target_size[1]}"
                    f":force_original_aspect_ratio=increase"
                    f",crop={target_size[0]}:{target_size[1]}"
                ),
                "-frames:v", "1",
                "-f", "image2pipe",
                "-vcodec", "mjpeg",
                "-"
            ]

            # Run ffmpeg with timeout
            process = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.DEVNULL,
            )

            try:
                stdout, _ = await asyncio.wait_for(process.communicate(), timeout=10.0)
                if process.returncode == 0 and stdout:
                    # ffmpeg output is already JPEG, but let's ensure proper quality
                    img = Image.open(BytesIO(stdout))

                    # Convert to RGB if necessary
                    if img.mode != "RGB":
                        img = img.convert("RGB")

                    # Save as JPEG with consistent quality
                    output = BytesIO()
                    img.save(output, format="JPEG", quality=85, optimize=True)
                    return output.getvalue()

            except asyncio.TimeoutError:
                logger.warning(f"ffmpeg timeout for {file_path.name}")
                process.kill()
                await process.wait()

            return None

        except ImportError:
            logger.error("Pillow library not installed")
            return None
        except Exception as e:
            logger.debug(f"Error generating video thumbnail for {file_path.name}: {e}")
            return None

    @staticmethod
    async def get_thumbnail(file_path: Path, size: str = "medium") -> Optional[bytes]:
        """Get thumbnail for a media file (from cache or generate).

        Args:
            file_path: Path to the media file.
            size: Thumbnail size ("small" or "medium").

        Returns:
            Thumbnail bytes (JPEG) or None if unavailable.
        """
        from .browser_service import AUDIO_EXTENSIONS, VIDEO_EXTENSIONS

        # Validate size
        if size not in THUMBNAIL_SIZES:
            size = "medium"

        # Check cache first (returns bytes, empty bytes for negative cache, or None)
        cached = ThumbnailService.get_cached_thumbnail(file_path, size)
        if cached is not None:
            return cached if cached else None

        # Generate thumbnail based on file type
        suffix = file_path.suffix.lower()
        thumbnail_data = None

        if suffix in AUDIO_EXTENSIONS:
            # Audio files - run in executor (sync operation)
            loop = asyncio.get_running_loop()
            thumbnail_data = await loop.run_in_executor(
                None,
                ThumbnailService.generate_audio_thumbnail,
                file_path,
                size,
            )
        elif suffix in VIDEO_EXTENSIONS:
            # Video files - already async
            thumbnail_data = await ThumbnailService.generate_video_thumbnail(file_path, size)

        # Cache result (positive or negative)
        if thumbnail_data:
            ThumbnailService.cache_thumbnail(file_path, size, thumbnail_data)
        else:
            ThumbnailService.cache_no_thumbnail(file_path)

        return thumbnail_data

    @staticmethod
    def cleanup_cache() -> None:
        """Clean up cache if it exceeds size limit.

        Removes oldest thumbnails by access time.
        """
        cache_dir = ThumbnailService.get_cache_dir()

        try:
            # Calculate total cache size
            total_size = 0
            cache_items = []

            for folder in cache_dir.iterdir():
                if folder.is_dir():
                    for file in folder.iterdir():
                        if file.is_file():
                            stat = file.stat()
                            total_size += stat.st_size
                            cache_items.append((file, stat.st_atime, stat.st_size))

            # If cache is within limit, no cleanup needed
            if total_size <= CACHE_SIZE_LIMIT:
                return

            logger.info(f"Cache size {total_size / 1024 / 1024:.2f}MB exceeds limit, cleaning up...")

            # Sort by access time (oldest first)
            cache_items.sort(key=lambda x: x[1])

            # Remove oldest items until under limit
            for file, _, size in cache_items:
                if total_size <= CACHE_SIZE_LIMIT:
                    break

                try:
                    file.unlink()
                    total_size -= size
                    logger.debug(f"Removed cached thumbnail: {file}")

                    # Remove empty parent folder
                    parent = file.parent
                    if parent != cache_dir and not any(parent.iterdir()):
                        parent.rmdir()

                except (OSError, PermissionError) as e:
                    logger.error(f"Error removing cache file: {e}")

            logger.info(f"Cache cleanup complete, new size: {total_size / 1024 / 1024:.2f}MB")

        except Exception as e:
            logger.error(f"Error during cache cleanup: {e}")
