import base64
import io

import aiohttp
from PIL import Image, ImageDraw


async def fetch_image(
    url: str,
    session: aiohttp.ClientSession | None = None,
    timeout: int = 10,
) -> Image.Image | None:
    """Fetch an image from a URL or data-URI and return a PIL Image."""
    if not url:
        return None

    # Handle data URIs (e.g. "data:image/png;base64,...")
    if url.startswith("data:"):
        try:
            _, encoded = url.split(",", 1)
            raw = base64.b64decode(encoded)
            return Image.open(io.BytesIO(raw)).convert("RGBA")
        except Exception:
            return None

    try:
        ct = aiohttp.ClientTimeout(total=timeout)
        headers = {"Referer": "https://bgm.tv/"}
        if session and not session.closed:
            async with session.get(url, timeout=ct, headers=headers) as resp:
                if resp.status == 200:
                    data = await resp.read()
                    return Image.open(io.BytesIO(data)).convert("RGBA")
        else:
            async with aiohttp.ClientSession() as s:
                async with s.get(url, timeout=ct, headers=headers) as resp:
                    if resp.status == 200:
                        data = await resp.read()
                        return Image.open(io.BytesIO(data)).convert("RGBA")
    except Exception:
        pass
    return None


def image_to_base64(img: Image.Image) -> str:
    """Convert a PIL Image to a base64-encoded PNG string."""
    buf = io.BytesIO()
    img.convert("RGB").save(buf, format="PNG", optimize=True)
    return base64.b64encode(buf.getvalue()).decode("utf-8")


def fit_cover(img: Image.Image, width: int, height: int) -> Image.Image:
    """Resize and center-crop to fill the target box (CSS object-fit: cover)."""
    src_ratio = img.width / img.height
    tgt_ratio = width / height
    if src_ratio > tgt_ratio:
        new_h, new_w = height, int(height * src_ratio)
    else:
        new_w, new_h = width, int(width / src_ratio)
    img = img.resize((new_w, new_h), Image.LANCZOS)
    left = (new_w - width) // 2
    top = (new_h - height) // 2
    return img.crop((left, top, left + width, top + height))


def rounded_clip(img: Image.Image, radius: int) -> Image.Image:
    """Apply rounded corners to an image via an alpha mask."""
    img = img.convert("RGBA")
    mask = Image.new("L", img.size, 0)
    ImageDraw.Draw(mask).rounded_rectangle(
        [0, 0, img.width - 1, img.height - 1], radius=radius, fill=255
    )
    result = Image.new("RGBA", img.size, (0, 0, 0, 0))
    result.paste(img, mask=mask)
    return result


def placeholder_cover(width: int, height: int, color: tuple[int, int, int] = (224, 224, 224)) -> Image.Image:
    """Create a solid-color placeholder cover."""
    return Image.new("RGBA", (width, height), (*color, 255))


def text_width(text: str, font: object) -> int:
    """Return the rendered width of *text* in pixels."""
    if hasattr(font, "getlength"):
        return int(font.getlength(text))  # type: ignore[union-attr]
    try:
        return font.getsize(text)[0]  # type: ignore[union-attr]
    except Exception:
        size = getattr(font, "size", 12)
        return len(text) * size


def text_line_height(font: object) -> int:
    """Return the line height (ascent + descent) of a font."""
    try:
        ascent, descent = font.getmetrics()  # type: ignore[union-attr]
        return ascent + descent
    except Exception:
        return getattr(font, "size", 12) + 4


def wrap_text(text: str, font: object, max_width: int) -> list[str]:
    """Word-wrap *text* to fit within *max_width* pixels (character-level wrap for CJK)."""
    lines: list[str] = []
    for paragraph in text.split("\n"):
        if not paragraph:
            lines.append("")
            continue
        line = ""
        for char in paragraph:
            candidate = line + char
            if text_width(candidate, font) <= max_width:
                line = candidate
            else:
                if line:
                    lines.append(line)
                line = char
        if line:
            lines.append(line)
    return lines or [""]


def strip_supplementary(text: str) -> str:
    """Remove supplementary-plane characters (emoji above U+FFFF) that most system fonts can't render."""
    return "".join(c for c in text if ord(c) <= 0xFFFF)


def make_gradient_overlay(width: int, height: int, start_pct: float = 0.4) -> Image.Image:
    """
    Create a vertical black-to-opaque gradient overlay.
    The first *start_pct* of the height is fully transparent; the remainder fades to ~95% opacity.
    """
    strip = Image.new("L", (1, 256))
    for i in range(256):
        pct = i / 255
        if pct < start_pct:
            v = 0
        else:
            progress = (pct - start_pct) / (1.0 - start_pct)
            v = min(255, int(progress * 242))
        strip.putpixel((0, i), v)
    alpha_strip = strip.resize((1, height), Image.BICUBIC)
    alpha_full = alpha_strip.resize((width, height), Image.NEAREST)
    overlay = Image.new("RGBA", (width, height), (0, 0, 0, 255))
    overlay.putalpha(alpha_full)
    return overlay
