"""Design report generator with image_grid components.

Creates one long image composed of multiple MxN image grids (no gaps inside
each grid), with optional spacing between grids. Can export to PNG and a
single-page PDF that contains the full long image.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Sequence, Tuple
from PIL import Image
import io
import json
import logging
import os
import urllib.parse
import urllib.request

logger = logging.getLogger(__name__)


def _is_url(path: str) -> bool:
    try:
        parsed = urllib.parse.urlparse(path)
        return parsed.scheme in ("http", "https")
    except Exception:
        return False


def _load_image_any(path: str) -> Image.Image:
    """Load image from local path or HTTP(S) URL.

    Args:
        path: File path or URL
    Returns:
        PIL Image
    """
    if _is_url(path):
        logger.debug(f"Downloading image: {path}")
        with urllib.request.urlopen(path) as resp:  # nosec - caller controls URLs
            data = resp.read()
        return Image.open(io.BytesIO(data)).convert("RGB")
    else:
        p = Path(path)
        if not p.exists():
            raise FileNotFoundError(f"Image not found: {p}")
        return Image.open(p).convert("RGB")


def _resize_cover(img: Image.Image, target_w: int, target_h: int) -> Image.Image:
    """Resize image to cover target size, cropping centrally as needed.

    Ensures there are no empty gaps in the target tile.
    """
    src_w, src_h = img.size
    scale = max(target_w / src_w, target_h / src_h)
    new_w, new_h = int(src_w * scale), int(src_h * scale)
    resized = img.resize((new_w, new_h), Image.LANCZOS)

    # central crop to exact target
    left = (new_w - target_w) // 2
    top = (new_h - target_h) // 2
    right = left + target_w
    bottom = top + target_h
    return resized.crop((left, top, right, bottom))


@dataclass
class ImageGridSpec:
    rows: int
    cols: int
    images: Sequence[str]
    tile_width: int = 512
    tile_height: int = 512

    def compose(self, background: str | Tuple[int, int, int] = "#FFFFFF") -> Image.Image:
        """Compose this grid into a single image (no internal gaps)."""
        if self.rows <= 0 or self.cols <= 0:
            raise ValueError("rows and cols must be positive")
        canvas_w = self.cols * self.tile_width
        canvas_h = self.rows * self.tile_height
        canvas = Image.new("RGB", (canvas_w, canvas_h), background)

        # Fill cells row-major
        for idx in range(self.rows * self.cols):
            if idx >= len(self.images):
                break  # remaining cells left as background
            r = idx // self.cols
            c = idx % self.cols
            try:
                img = _load_image_any(str(self.images[idx]))
            except Exception as e:
                logger.warning(f"Failed to load image at index {idx}: {e}")
                continue
            tile = _resize_cover(img, self.tile_width, self.tile_height)
            x = c * self.tile_width
            y = r * self.tile_height
            canvas.paste(tile, (x, y))

        return canvas


@dataclass
class DesignReportSpec:
    grids: Sequence[ImageGridSpec]
    grid_spacing: int = 24
    page_margin: int = 24
    background: str | Tuple[int, int, int] = "#FFFFFF"


def compose_design_report(spec: DesignReportSpec) -> Image.Image:
    """Compose a design report from multiple grids into one long image."""
    if not spec.grids:
        raise ValueError("At least one grid is required")

    # Render each grid first
    rendered = [g.compose(background=spec.background) for g in spec.grids]
    max_w = max(img.size[0] for img in rendered)
    total_h = sum(img.size[1] for img in rendered)
    total_h += spec.grid_spacing * (len(rendered) - 1) if len(rendered) > 1 else 0

    canvas_w = max_w + 2 * spec.page_margin
    canvas_h = total_h + 2 * spec.page_margin
    canvas = Image.new("RGB", (canvas_w, canvas_h), spec.background)

    y = spec.page_margin
    for idx, img in enumerate(rendered):
        # Left-align each grid; change to center if desired
        x = spec.page_margin
        canvas.paste(img, (x, y))
        y += img.size[1]
        if idx < len(rendered) - 1:
            y += spec.grid_spacing

    return canvas


def save_design_report(
    image: Image.Image,
    output_png: Optional[str | os.PathLike] = None,
    output_pdf: Optional[str | os.PathLike] = None,
    pdf_resolution: int = 72,
) -> Tuple[Optional[Path], Optional[Path]]:
    """Save the composed report to PNG and/or single-page PDF.

    Returns the saved paths (None if not saved).
    """
    png_path: Optional[Path] = None
    pdf_path: Optional[Path] = None

    if output_png:
        png_path = Path(output_png)
        png_path.parent.mkdir(parents=True, exist_ok=True)
        image.save(png_path, format="PNG")
        logger.info(f"Saved PNG to {png_path}")

    if output_pdf:
        pdf_path = Path(output_pdf)
        pdf_path.parent.mkdir(parents=True, exist_ok=True)
        # Save as a single long page that matches the image size
        image.save(pdf_path, format="PDF", resolution=pdf_resolution, save_all=False)
        logger.info(f"Saved PDF to {pdf_path}")

    return png_path, pdf_path


def build_design_report_from_config(config_path: str) -> Tuple[Image.Image, Optional[Path], Optional[Path]]:
    """Build a design report from a JSON config file.

    Config schema example:
    {
      "grids": [
        {"rows": 2, "cols": 3, "images": ["a.png", "b.png", ...], "tile_width": 512, "tile_height": 512},
        {"rows": 1, "cols": 4, "images": ["..."], "tile_width": 512, "tile_height": 512}
      ],
      "grid_spacing": 24,
      "page_margin": 24,
      "background": "#FFFFFF",
      "output_png": "report.png",
      "output_pdf": "report.pdf"
    }
    """
    with open(config_path, "r", encoding="utf-8") as f:
        cfg = json.load(f)

    grids_cfg = cfg.get("grids")
    if not grids_cfg or not isinstance(grids_cfg, list):
        raise ValueError("config.grids must be a non-empty list")

    grids: List[ImageGridSpec] = []
    for item in grids_cfg:
        grids.append(
            ImageGridSpec(
                rows=int(item["rows"]),
                cols=int(item["cols"]),
                images=item.get("images", []),
                tile_width=int(item.get("tile_width", 512)),
                tile_height=int(item.get("tile_height", 512)),
            )
        )

    spec = DesignReportSpec(
        grids=grids,
        grid_spacing=int(cfg.get("grid_spacing", 24)),
        page_margin=int(cfg.get("page_margin", 24)),
        background=cfg.get("background", "#FFFFFF"),
    )

    image = compose_design_report(spec)
    png_path, pdf_path = save_design_report(
        image,
        output_png=cfg.get("output_png"),
        output_pdf=cfg.get("output_pdf"),
    )
    return image, png_path, pdf_path
