"""Convert images to ICO format.

This module provides functionality to convert various image formats to ICO files.
It supports multiple icon sizes and can process single files or batch convert
images in a directory.

Command: img2ico [--sizes SIZES] [input_path] [output_path]
"""

from __future__ import annotations

import argparse
import logging
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import ClassVar

from PIL import Image
from PIL.Image import Resampling
from pytola_core.config import ConfigMixin

# Optional SVG support
try:
    import cairosvg

    CAIROSVG_AVAILABLE = True
except (ImportError, OSError) as e:
    # ImportError: cairosvg not installed
    # OSError: Cairo system libraries missing (common on Windows)
    CAIROSVG_AVAILABLE = False
    # Store error for better debugging
    _CAIROSVG_ERROR = str(e)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class ImageToIcoConfig(ConfigMixin):
    """Configuration for image to ICO conversion."""

    DEFAULT_SIZES: ClassVar[set[int]] = {16, 24, 32, 48, 64, 128, 256, 512, 1024}
    EXTENSIONS: ClassVar[set[str]] = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".svg"}
    MAX_ICON_SIZE = 256


conf = ImageToIcoConfig()


# Magic numbers for image file headers.
_MAGIC_NUMBERS: dict[str, bytes] = {
    "jpg": b"\xff\xd8\xff",
    "jpeg": b"\xff\xd8\xff",
    "png": b"\x89PNG\r\n\x1a\n",
    "gif": b"GIF87a",
    "bmp": b"BM",
    "webp": b"RIFFf\x00\x00\x00WEBP",
    "tiff": b"II*\x00",
    "ico": b"\x00\x00\x01\x00",
    "svg": b"<?xml",  # SVG files typically start with XML declaration
}


def is_valid_image(file_path: Path) -> bool:
    """Validate image file.

    Args:
        file_path: Path to the image file to validate

    Returns
    -------
        bool: True if the file is a valid image file, False otherwise
    """
    # Basic validation.
    try:
        stat_result = file_path.stat()
        if stat_result.st_size == 0:
            logger.debug(f"Empty file: {file_path}")
            return False
    except OSError:
        logger.debug(f"File not found or inaccessible: {file_path}")
        return False

    # Extension validation.
    ext = file_path.suffix.lower()
    if not conf.EXTENSIONS or ext not in conf.EXTENSIONS:
        logger.debug(f"Invalid image extension: {ext}, {file_path}")
        return False

    # File header validation.
    try:
        with file_path.open("rb") as f:
            header = f.read(16)
            if not any(header.startswith(v) for v in _MAGIC_NUMBERS.values()):
                logger.debug(f"Invalid image header: {header[:8]}")
                return False
    except OSError:
        logger.debug(f"Cannot read file header: {file_path}")
        return False

    logger.info(f"Valid image: {file_path}")
    return True


def parse_sizes(sizes_str: str) -> set[int]:
    """Parse size string into set of integers.

    Args:
        sizes_str: Comma-separated string of sizes (e.g., "16,32,48,256")

    Returns
    -------
        Set of integer sizes

    Raises
    ------
        ValueError: If sizes string is invalid
    """

    def validate_and_parse_sizes(sizes_str: str) -> set[int]:
        """Validate and parse sizes string."""
        # First check if the string is empty or only contains commas/whitespace
        stripped_parts = [s.strip() for s in sizes_str.split(",")]
        if not any(stripped_parts):
            msg = "No valid sizes provided"
            raise ValueError(msg)

        try:
            sizes = {int(s) for s in stripped_parts if s}
        except ValueError as e:
            msg = f"Invalid sizes format: {sizes_str}. Expected comma-separated integers."
            raise ValueError(msg) from e
        else:
            if not sizes:
                msg = "No valid sizes provided"
                raise ValueError(msg)
        return sizes

    return validate_and_parse_sizes(sizes_str)


@dataclass(frozen=True)
class ImageToIcoRunner:
    """Image to ICO converter processor.

    Processes image files and converts them to ICO format with multiple sizes.
    Supports both single file and batch directory conversion.
    """

    input_path: Path  # Input file or directory
    output_path: Path | None = None  # Output ICO file or directory
    sizes: set[int] | None = None  # Icon sizes to generate (optional, uses DEFAULT_SIZES if None)
    quality: str = "high"  # Image quality: "low", "medium", "high"

    def __post_init__(self) -> None:
        """Post-initialization hook to set default sizes if none provided."""
        object.__setattr__(self, "sizes", self.sizes or conf.DEFAULT_SIZES)

    def run(self) -> None:
        """Execute the image to ICO conversion process."""
        if self.input_path.is_file():
            self._convert_single_file(self.input_path, self.output_path)
        elif self.input_path.is_dir():
            self._convert_directory()
        else:
            logger.error(f"Input path does not exist: {self.input_path}")

    def _convert_single_file(self, input_file: Path, output_file: Path | None) -> None:
        """Convert a single image file to ICO format.

        Args:
            input_file: Path to the input image file
            output_file: Path to the output ICO file (optional)
        """
        if not is_valid_image(input_file):
            logger.error(f"Invalid image file: {input_file}")
            return

        # Handle SVG files specially
        if input_file.suffix.lower() == ".svg":
            self._convert_svg_file(input_file, output_file)
            return

        # Determine output path
        if output_file is None:
            output_file = input_file.with_suffix(".ico")
        elif output_file.exists() and output_file.is_dir():
            # If output_file is an existing directory, create file inside it
            output_file /= f"{input_file.stem}.ico"
        elif not output_file.exists() and not output_file.suffix:
            # If output_file doesn't exist and has no suffix, treat it as a directory
            output_file.mkdir(parents=True, exist_ok=True)
            output_file /= f"{input_file.stem}.ico"
        # else: output_file is treated as a file path

        # Ensure output directory exists
        output_file.parent.mkdir(parents=True, exist_ok=True)

        try:
            # Open and convert image
            with Image.open(str(input_file)) as original_img:
                # Convert to RGBA to support transparency
                img = original_img.convert("RGBA") if original_img.mode != "RGBA" else original_img

                # Generate icon images for each size
                icon_images = []
                for size in sorted(self.sizes or conf.DEFAULT_SIZES):
                    # Resize image maintaining aspect ratio
                    resized = self._resize_for_icon(img, size)
                    icon_images.append(resized)

                # Save ICO file with all sizes
                # Save ICO file with all sizes using our custom method
                self._save_multisize_ico(icon_images, sorted(self.sizes or conf.DEFAULT_SIZES), output_file)

            logger.info(f"Created ICO file: {output_file}")
            logger.info(f"Icon sizes: {sorted(self.sizes or conf.DEFAULT_SIZES)}")

        except (OSError, RuntimeError, ValueError):
            logger.exception(f"Failed to convert {input_file}")

    def _convert_directory(self) -> None:
        """Convert all images in a directory to ICO files."""
        image_files = self.image_files
        if not image_files:
            logger.warning(f"No valid image files found in: {self.input_path}")
            return

        # Determine output directory
        output_dir = self.output_path or self.input_path / "ico_output"
        output_dir.mkdir(parents=True, exist_ok=True)

        logger.info(f"Converting {len(image_files)} images to ICO format")

        for image_file in image_files:
            output_file = output_dir / f"{image_file.stem}.ico"
            self._convert_single_file(image_file, output_file)

    def _resize_for_icon(self, image: Image.Image, size: int) -> Image.Image:
        """Resize image to fit icon dimensions while maintaining aspect ratio.

        Uses high-quality resampling and sharpening to preserve image details.
        Quality modes:
        - low: Fastest, uses NEAREST resampling
        - medium: Balanced, uses BILINEAR resampling
        - high: Best quality, uses LANCZOS/BICUBIC with sharpening

        Args:
            image: PIL Image object to resize
            size: Target size for the icon (width and height)

        Returns
        -------
            Resized PIL Image object with transparent background
        """
        from PIL import ImageFilter

        # Calculate resize dimensions maintaining aspect ratio
        img_width, img_height = image.size
        ratio = min(size / img_width, size / img_height)
        new_width = int(img_width * ratio)
        new_height = int(img_height * ratio)

        if self.quality == "low":
            # Fast but lower quality
            resized = image.resize((new_width, new_height), Resampling.NEAREST)
        elif self.quality == "medium":
            # Balanced quality and speed
            resized = image.resize((new_width, new_height), Resampling.BILINEAR)
        # For upscaling small images, use BICUBIC with sharpening
        elif ratio > 1:
            resized = image.resize((new_width, new_height), Resampling.BICUBIC)
            # Apply slight sharpening to compensate for upscaling blur
            resized = resized.filter(ImageFilter.SHARPEN)
        elif ratio < 0.25:
            # For extreme downscaling, use multi-step LANCZOS
            # First resize to 4x target size, then 2x, then final
            intermediate1 = image.resize((size * 4, size * 4), Resampling.LANCZOS)
            intermediate2 = intermediate1.resize((size * 2, size * 2), Resampling.LANCZOS)
            resized = intermediate2.resize((new_width, new_height), Resampling.LANCZOS)
        elif ratio < 0.5:
            # For heavy downscaling, use two-step LANCZOS
            intermediate = image.resize((size * 2, size * 2), Resampling.LANCZOS)
            resized = intermediate.resize((new_width, new_height), Resampling.LANCZOS)
        else:
            # Normal downscaling with LANCZOS (best for reduction)
            resized = image.resize((new_width, new_height), Resampling.LANCZOS)

        # Create new image with transparent background
        icon_img = Image.new("RGBA", (size, size), (0, 0, 0, 0))

        # Center the resized image
        x_offset = (size - new_width) // 2
        y_offset = (size - new_height) // 2
        icon_img.paste(resized, (x_offset, y_offset), resized)

        return icon_img

    def _save_multisize_ico(self, icon_images: list[Image.Image], sizes: list[int], output_file: Path) -> None:
        """保存真正的多尺寸 ICO 文件, 使用手工构造方法确保兼容性.

        Args:
            icon_images: 图像对象列表
            sizes: 对应的尺寸列表
            output_file: 输出文件路径
        """
        logger.info(f"开始保存多尺寸 ICO: {len(icon_images)} 个图像, 尺寸: {sizes}")
        # 直接使用手工构造方法, 因为 PIL 的多尺寸支持不可靠
        logger.info("使用手工构造方法创建真正的多尺寸 ICO")
        self._create_genuine_multisize_ico(icon_images, sizes, output_file)

    def _create_genuine_multisize_ico(
        self,
        icon_images: list[Image.Image],
        sizes: list[int],
        output_file: Path,
    ) -> None:
        """创建真正的多尺寸 ICO 文件.

        直接将每个图像保存为 PNG, 然后手动构造包含所有尺寸的 ICO 文件结构.
        现代 ICO 格式支持将 PNG 数据直接嵌入，这是 Windows Vista 后的标准做法.
        """
        import io
        import struct

        def validate_icon_data(png_data_list: list[tuple[bytes, int]]) -> None:
            """Validate icon data before creating ICO file."""
            if not png_data_list:
                msg = "没有有效的图像数据"
                raise ValueError(msg)

        logger.info(f"开始手工构造多尺寸 ICO: {len(icon_images)} 个图像")

        # 为每个尺寸保存为 PNG 数据
        png_data_list = []
        for img, size in zip(icon_images, sizes):
            # 保存为 PNG 格式到内存缓冲区
            png_buffer = io.BytesIO()
            img.save(png_buffer, format="PNG")
            png_data = png_buffer.getvalue()
            png_data_list.append((png_data, size))
            png_buffer.close()
            logger.debug(f"保存尺寸 {size}x{size} 为 PNG ({len(png_data)} bytes)")

        # Validate icon data
        validate_icon_data(png_data_list)

        logger.info(f"收集到 {len(png_data_list)} 个尺寸的 PNG 数据")

        # 构造多尺寸 ICO 文件结构
        # ICO 文件头: Reserved(2) + Type(2) + Count(2)
        count = len(png_data_list)
        header = struct.pack("<HHH", 0, 1, count)  # 0=Reserved, 1=ICO type

        # 构建目录项
        directory_entries = b""
        image_data_blocks = []
        current_offset = 6 + count * 16  # 头部 + 所有目录项大小

        for png_data, size in png_data_list:
            # 对于 PNG 格式的 ICO 图像, 目录项需要设置特殊标记
            # Width/Height 为 0 表示 256 或更大尺寸
            width = size if size < conf.MAX_ICON_SIZE else 0
            height = size if size < conf.MAX_ICON_SIZE else 0

            # PNG 图像的目录项:
            # - Width/Height: 实际尺寸(256以上用0表示)
            # - ColorCount: 0(PNG 不使用调色板)
            # - Reserved: 0
            # - Planes: 1(对于 PNG 通常为 1)
            # - BitCount: 32(RGBA)
            # - Size: PNG 数据的实际大小
            # - Offset: PNG 数据的偏移量

            directory_entry = struct.pack(
                "<BBBBHHII",
                width & 0xFF,  # Width (1 byte)
                height & 0xFF,  # Height (1 byte)
                0,  # ColorCount (1 byte, 0 for PNG)
                0,  # Reserved (1 byte)
                1,  # Planes (2 bytes, typically 1)
                32,  # BitCount (2 bytes, 32 for RGBA)
                len(png_data),  # Size in bytes (4 bytes)
                current_offset,  # Offset to image data (4 bytes)
            )

            directory_entries += directory_entry
            image_data_blocks.append(png_data)
            current_offset += len(png_data)
            logger.debug(f"添加尺寸 {size}x{size} PNG 到目录 (偏移: {current_offset}, 大小: {len(png_data)} bytes)")

        # 组合完整的 ICO 文件
        ico_data = header + directory_entries
        for image_data in image_data_blocks:
            ico_data += image_data

        # 保存最终的多尺寸 ICO 文件
        Path(output_file).write_bytes(ico_data)

        logger.info(f"✅ 真正的多尺寸 ICO 创建成功: {output_file}")
        logger.info(f"   包含尺寸: {[size for _, size in png_data_list]}")
        logger.info(f"   文件大小: {len(ico_data)} 字节")
        logger.info(f"   目录项数量: {count}")

    @cached_property
    def image_files(self) -> list[Path]:
        """Get list of valid image files in the input directory.

        Returns
        -------
            List of Path objects representing valid image files, sorted alphabetically.
        """
        all_files = list(self.input_path.iterdir())
        image_filepath = sorted([file for file in all_files if is_valid_image(file)])

        if not image_filepath:
            logger.warning(f"No valid image files found in: {self.input_path}")
            logger.info(f"Total files in directory: {len(all_files)}")
        else:
            logger.info(f"Found {len(image_filepath)} valid image files out of {len(all_files)} total files")

        return image_filepath

    def _convert_svg_file(self, svg_file: Path, output_file: Path | None) -> None:
        """Convert SVG file to ICO format using cairosvg.

        Args:
            svg_file: Path to the input SVG file
            output_file: Path to the output ICO file (optional)
        """
        if not CAIROSVG_AVAILABLE:
            error_msg = (
                f"Cannot convert SVG file {svg_file}: cairosvg library not available. "
                "Install it with: pip install cairosvg"
            )
            if "_CAIROSVG_ERROR" in globals():
                error_msg += f"\nError details: {_CAIROSVG_ERROR}"
            logger.error(error_msg)
            return

        # Determine output path
        if output_file is None:
            output_file = svg_file.with_suffix(".ico")
        elif output_file.exists() and output_file.is_dir():
            # If output_file is an existing directory, create file inside it
            output_file /= f"{svg_file.stem}.ico"
        elif not output_file.exists() and not output_file.suffix:
            # If output_file doesn't exist and has no suffix, treat it as a directory
            output_file.mkdir(parents=True, exist_ok=True)
            output_file /= f"{svg_file.stem}.ico"
        # else: output_file is treated as a file path

        # Ensure output directory exists
        output_file.parent.mkdir(parents=True, exist_ok=True)

        try:
            # Convert SVG to PNG in memory first
            png_data = cairosvg.svg2png(url=str(svg_file))

            # Open PNG data as PIL Image
            from io import BytesIO

            png_image = Image.open(BytesIO(png_data))

            # Convert to RGBA to support transparency
            if png_image.mode != "RGBA":
                png_image = png_image.convert("RGBA")

            # Generate icon images for each size
            icon_images = []
            for size in sorted(self.sizes or conf.DEFAULT_SIZES):
                # Resize image maintaining aspect ratio
                resized = self._resize_for_icon(png_image, size)
                icon_images.append(resized)

            # Save ICO file with all sizes using our custom method
            self._save_multisize_ico(icon_images, sorted(self.sizes or conf.DEFAULT_SIZES), output_file)

            logger.info(f"Created ICO file from SVG: {output_file}")
            logger.info(f"Icon sizes: {sorted(self.sizes or conf.DEFAULT_SIZES)}")

        except (OSError, RuntimeError, ValueError):
            logger.exception(f"Failed to convert SVG {svg_file}")


def parse_args() -> argparse.Namespace:
    """Parse command line arguments for the img2ico tool.

    Returns
    -------
        Namespace object containing parsed arguments:
        - input: Path to the input image file or directory
        - output: Path to the output ICO file or directory
        - sizes: Set of icon sizes to generate
    """
    parser = argparse.ArgumentParser(description="Convert images to ICO format with multiple sizes.")
    parser.add_argument(
        "input",
        type=str,
        nargs="?",
        default=str(Path.cwd()),
        help="Input image file or directory (default: current directory)",
    )
    parser.add_argument(
        "output",
        type=str,
        nargs="?",
        default=None,
        help="Output ICO file or directory (default: input_name.ico or ico_output/)",
    )
    parser.add_argument(
        "--sizes",
        "-s",
        type=str,
        default=None,
        help="Comma-separated list of icon sizes (default: Windows&macOS optimized - 16,24,32,48,64,128,256,512,1024)",
    )
    parser.add_argument(
        "--quality",
        "-q",
        type=str,
        choices=["low", "medium", "high"],
        default="high",
        help="Image resize quality: low (fast), medium (balanced), high (best, default)",
    )
    return parser.parse_args()


def main() -> None:
    """Run main entry point for the img2ico command line tool.

    Parses command line arguments, validates the input path,
    and executes the image to ICO conversion process.
    """
    args = parse_args()

    # Convert string paths to Path objects
    input_path = Path(args.input)
    output_path = Path(args.output) if args.output else None

    # Parse sizes if provided
    sizes = None
    if args.sizes:
        try:
            sizes = parse_sizes(args.sizes)
        except ValueError:
            logger.exception("Invalid sizes argument")
            return

    # Validate input path exists
    if not input_path.exists():
        logger.error(f"Input path does not exist: {input_path}")
        return

    # Execute conversion
    runner = ImageToIcoRunner(input_path=input_path, output_path=output_path, sizes=sizes, quality=args.quality)
    runner.run()


if __name__ == "__main__":
    main()
