from __future__ import annotations

import hashlib
import math
import random
from pathlib import Path

from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont

from src.util.text import hex_to_rgb, wrap_text

SIZE_SQUARE = (1080, 1080)

LAYOUTS = (
    "magazine_cover",
    "editorial_overlay",
    "apple_keynote",
    "pitch_deck",
    "bento_grid",
    "hero_banner",
    "infographic",
    "news_broadcast",
    "timeline",
    "dashboard_ui",
    "split_comparison",
    "glass_card",
    "minimal_premium",
    "neon_tech",
    "bold_stat",
    "gradient_story",
    "corporate_linkedin",
    "cyberpunk",
    "split_diagonal",
)

_LAYOUT_ALIASES: dict[str, str] = {
    "keynote": "apple_keynote",
    "apple": "apple_keynote",
    "deck": "pitch_deck",
    "pitch": "pitch_deck",
    "bento": "bento_grid",
    "grid": "bento_grid",
    "banner": "hero_banner",
    "hero": "hero_banner",
    "info": "infographic",
    "infographic_card": "infographic",
    "news": "news_broadcast",
    "broadcast": "news_broadcast",
    "live": "news_broadcast",
    "timeline_card": "timeline",
    "dashboard": "dashboard_ui",
    "ui": "dashboard_ui",
    "analytics": "dashboard_ui",
    "comparison": "split_comparison",
    "versus": "split_comparison",
    "vs": "split_comparison",
    "split": "split_diagonal",
    "diagonal": "split_diagonal",
    "glass": "glass_card",
    "card": "glass_card",
    "minimal": "minimal_premium",
    "premium": "minimal_premium",
    "neon": "neon_tech",
    "tech": "neon_tech",
    "stat": "bold_stat",
    "bold": "bold_stat",
    "gradient": "gradient_story",
    "story": "gradient_story",
    "linkedin": "corporate_linkedin",
    "corporate": "corporate_linkedin",
    "business": "corporate_linkedin",
    "cyber": "cyberpunk",
    "punk": "cyberpunk",
    "magazine": "magazine_cover",
    "cover": "magazine_cover",
    "editorial": "editorial_overlay",
    "overlay": "editorial_overlay",
}


def _lerp(a: int, b: int, t: float) -> int:
    return int(a + (b - a) * t)


def _blend_rgb(c1: tuple[int, int, int], c2: tuple[int, int, int], t: float) -> tuple[int, int, int]:
    return (_lerp(c1[0], c2[0], t), _lerp(c1[1], c2[1], t), _lerp(c1[2], c2[2], t))


class VisualRenderer:
    """Agency-grade multi-layout compositor for premium social graphics."""

    def __init__(
        self,
        brand_name: str,
        primary: str,
        accent: str,
        bg: str,
        fonts_dir: Path | None = None,
    ):
        self.brand_name = brand_name
        self.primary = hex_to_rgb(primary)
        self.accent = hex_to_rgb(accent)
        self.bg = hex_to_rgb(bg)
        self.fonts_dir = fonts_dir

    # --- Shared helpers --------------------------------------------------------

    def _font(self, size: int, bold: bool = False) -> ImageFont.ImageFont:
        candidates: list[Path] = []
        if self.fonts_dir and self.fonts_dir.exists():
            candidates.extend(sorted(self.fonts_dir.glob("*.ttf")))
            candidates.extend(sorted(self.fonts_dir.glob("*.otf")))
        bold_paths = [
            Path("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"),
            Path("/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"),
        ]
        regular_paths = [
            Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"),
            Path("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"),
        ]
        candidates.extend(bold_paths if bold else regular_paths)
        for path in candidates:
            if path.exists():
                try:
                    return ImageFont.truetype(str(path), size=size)
                except OSError:
                    continue
        return ImageFont.load_default()

    def _seed(self, key: str) -> random.Random:
        h = int(hashlib.md5(key.encode()).hexdigest()[:8], 16)
        return random.Random(h)

    def _gradient(
        self,
        size: tuple[int, int],
        c1: tuple[int, int, int],
        c2: tuple[int, int, int],
        *,
        diagonal: bool = True,
        vertical: bool = False,
    ) -> Image.Image:
        w, h = size
        img = Image.new("RGB", size)
        px = img.load()
        for y in range(h):
            for x in range(0, w, 2):
                if vertical:
                    t = y / max(h - 1, 1)
                elif diagonal:
                    t = ((x / max(w - 1, 1)) + (y / max(h - 1, 1))) / 2
                else:
                    t = x / max(w - 1, 1)
                color = _blend_rgb(c1, c2, t)
                px[x, y] = color
                if x + 1 < w:
                    px[x + 1, y] = color
        return img

    def _radial_gradient(
        self,
        size: tuple[int, int],
        inner: tuple[int, int, int],
        outer: tuple[int, int, int],
        center: tuple[float, float] = (0.5, 0.4),
    ) -> Image.Image:
        w, h = size
        img = Image.new("RGB", size)
        px = img.load()
        cx, cy = center[0] * w, center[1] * h
        max_r = math.hypot(w, h)
        for y in range(0, h, 2):
            for x in range(0, w, 2):
                t = min(1.0, math.hypot(x - cx, y - cy) / (max_r * 0.65))
                color = _blend_rgb(inner, outer, t)
                px[x, y] = color
                if x + 1 < w:
                    px[x + 1, y] = color
                if y + 1 < h:
                    px[x, y + 1] = color
                    if x + 1 < w:
                        px[x + 1, y + 1] = color
        return img

    def _parse_palette(self, palette: list[str] | None) -> list[tuple[int, int, int]]:
        colors: list[tuple[int, int, int]] = []
        for hex_c in (palette or [])[:4]:
            try:
                colors.append(hex_to_rgb(hex_c))
            except Exception:
                continue
        if len(colors) < 2:
            colors = [self.bg, self.primary, self.accent, self.accent]
        return colors

    def _fit_cover(self, img: Image.Image, size: tuple[int, int]) -> Image.Image:
        tw, th = size
        iw, ih = img.size
        scale = max(tw / iw, th / ih)
        nw, nh = int(iw * scale), int(ih * scale)
        img = img.resize((nw, nh), Image.Resampling.LANCZOS)
        left = (nw - tw) // 2
        top = (nh - th) // 2
        return img.crop((left, top, left + tw, top + th))

    def _glass_panel(
        self,
        base: Image.Image,
        box: tuple[int, int, int, int],
        *,
        alpha: int = 160,
        tint: tuple[int, int, int] = (15, 23, 42),
        radius: int = 28,
        border: tuple[int, int, int, int] | None = (255, 255, 255, 50),
    ) -> Image.Image:
        rgba = base.convert("RGBA")
        overlay = Image.new("RGBA", rgba.size, (0, 0, 0, 0))
        draw = ImageDraw.Draw(overlay)
        x0, y0, x1, y1 = box
        draw.rounded_rectangle([x0, y0, x1, y1], radius=radius, fill=(*tint, alpha))
        if border:
            draw.rounded_rectangle([x0, y0, x1, y1], radius=radius, outline=border, width=2)
        return Image.alpha_composite(rgba, overlay).convert("RGB")

    def _scrim(
        self,
        size: tuple[int, int],
        *,
        direction: str = "bottom",
        height: int = 480,
        color: tuple[int, int, int] = (0, 0, 0),
        max_alpha: int = 230,
    ) -> Image.Image:
        w, h = size
        scrim = Image.new("RGBA", size, (0, 0, 0, 0))
        sd = ImageDraw.Draw(scrim)
        span = min(height, h)
        for i in range(span):
            a = int(max_alpha * (i / max(span - 1, 1)))
            if direction == "bottom":
                y = h - span + i
                sd.line([(0, y), (w, y)], fill=(*color, a))
            elif direction == "top":
                sd.line([(0, i), (w, i)], fill=(*color, a))
            elif direction == "full":
                sd.line([(0, i), (w, i)], fill=(*color, int(max_alpha * 0.55 * (i / max(span - 1, 1)))))
        return scrim

    def _apply_scrim(self, base: Image.Image, scrim: Image.Image) -> None:
        base.paste(Image.alpha_composite(base.convert("RGBA"), scrim).convert("RGB"))

    def _decorative_blobs(self, base: Image.Image, accent: tuple[int, int, int], rng: random.Random, size: tuple[int, int]) -> None:
        w, h = size
        for _ in range(3):
            cx, cy = rng.randint(0, w), rng.randint(0, h)
            r = rng.randint(100, 260)
            blob = Image.new("RGBA", size, (0, 0, 0, 0))
            bd = ImageDraw.Draw(blob)
            bd.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(*accent, 50))
            blob = blob.filter(ImageFilter.GaussianBlur(36))
            base.paste(Image.alpha_composite(base.convert("RGBA"), blob).convert("RGB"))

    def _resolve_layout(self, layout: str) -> str:
        key = (layout or "editorial_overlay").lower().replace("-", "_").strip()
        if key in LAYOUTS:
            return key
        if key in _LAYOUT_ALIASES:
            return _LAYOUT_ALIASES[key]
        for token, target in _LAYOUT_ALIASES.items():
            if token in key or key in token:
                return target
        return "editorial_overlay"

    def _typography_profile(self, typography: str | None) -> dict:
        key = (typography or "modern_startup").lower().replace("-", "_")
        profiles = {
            "modern_startup": {"hook_mul": 1.0, "body_mul": 1.0, "brand_caps": True, "hook_bold": True},
            "apple_minimal": {"hook_mul": 1.15, "body_mul": 0.88, "brand_caps": False, "hook_bold": True},
            "swiss": {"hook_mul": 0.95, "body_mul": 0.92, "brand_caps": True, "hook_bold": True},
            "luxury": {"hook_mul": 0.9, "body_mul": 0.85, "brand_caps": True, "hook_bold": False},
            "brutalist": {"hook_mul": 1.1, "body_mul": 1.05, "brand_caps": True, "hook_bold": True},
            "newspaper": {"hook_mul": 0.88, "body_mul": 0.95, "brand_caps": True, "hook_bold": True},
            "futuristic": {"hook_mul": 1.05, "body_mul": 0.9, "brand_caps": True, "hook_bold": True},
            "editorial": {"hook_mul": 0.98, "body_mul": 0.95, "brand_caps": True, "hook_bold": True},
        }
        return profiles.get(key, profiles["modern_startup"])

    def _text_limits(self, has_bg: bool) -> dict[str, int]:
        if has_bg:
            return {"hook_lines": 3, "hook_wrap": 18, "body_lines": 2, "body_wrap": 34}
        return {"hook_lines": 4, "hook_wrap": 22, "body_lines": 5, "body_wrap": 42}

    def _draw_lines(
        self,
        draw: ImageDraw.ImageDraw,
        x: int,
        y: int,
        text: str,
        *,
        font: ImageFont.ImageFont,
        fill: tuple[int, int, int],
        wrap: int,
        max_lines: int,
        line_height: int,
    ) -> int:
        for line in wrap_text(text, wrap)[:max_lines]:
            draw.text((x, y), line, fill=fill, font=font)
            y += line_height
        return y

    def _build_base(
        self,
        size: tuple[int, int],
        colors: list[tuple[int, int, int]],
        bg_image: Path | None,
        rng: random.Random,
    ) -> tuple[Image.Image, bool]:
        w, h = size
        has_bg = bool(bg_image and bg_image.exists())
        if has_bg:
            base = self._fit_cover(Image.open(bg_image).convert("RGB"), size)
            base = ImageEnhance.Brightness(base).enhance(0.52)
            base = ImageEnhance.Contrast(base).enhance(1.18)
            shade = Image.new("RGB", size, (0, 0, 0))
            base = Image.blend(base, shade, 0.28)
        else:
            style = rng.choice(["diag", "vert", "radial"])
            c1, c2 = colors[0], colors[min(1, len(colors) - 1)]
            if style == "radial":
                base = self._radial_gradient(size, c2, c1)
            elif style == "vert":
                base = self._gradient(size, c1, c2, vertical=True)
            else:
                base = self._gradient(size, c1, c2, diagonal=True)
            self._decorative_blobs(base, colors[-1], rng, size)
        return base, has_bg

    # --- Public API ------------------------------------------------------------

    def compose_hero(
        self,
        out_path: Path,
        *,
        hook: str,
        body: str,
        footer: str | None = None,
        layout: str = "editorial_overlay",
        palette: list[str] | None = None,
        bg_image: Path | None = None,
        creative_key: str = "default",
        size: tuple[int, int] = SIZE_SQUARE,
        typography: str | None = None,
    ) -> Path:
        layout = self._resolve_layout(layout)
        colors = self._parse_palette(palette)
        rng = self._seed(creative_key + layout)
        typo = self._typography_profile(typography)
        limits = self._text_limits(bool(bg_image and bg_image.exists()))

        base, has_bg = self._build_base(size, colors, bg_image, rng)
        draw = ImageDraw.Draw(base)
        renderer = getattr(self, f"_layout_{layout}", self._layout_editorial_overlay)
        renderer(
            draw,
            base,
            hook=hook,
            body=body,
            footer=footer or self.brand_name,
            colors=colors,
            rng=rng,
            size=size,
            typo=typo,
            limits=limits,
            has_bg=has_bg,
        )

        out_path.parent.mkdir(parents=True, exist_ok=True)
        base.convert("RGB").save(out_path, format="PNG", optimize=True)
        return out_path

    def render_single(
        self,
        out_path: Path,
        hook: str,
        body: str,
        footer: str | None = None,
        size=SIZE_SQUARE,
        **kwargs,
    ) -> Path:
        return self.compose_hero(
            out_path,
            hook=hook,
            body=body,
            footer=footer,
            layout=kwargs.get("layout", "editorial_overlay"),
            palette=kwargs.get("palette"),
            bg_image=kwargs.get("bg_image"),
            creative_key=kwargs.get("creative_key", hook),
            size=size,
            typography=kwargs.get("typography"),
        )

    def render_carousel_slide(
        self,
        out_path: Path,
        index: int,
        total: int,
        title: str,
        body: str,
        size=SIZE_SQUARE,
        **kwargs,
    ) -> Path:
        layouts = list(LAYOUTS)
        layout = kwargs.get("layout") or layouts[index % len(layouts)]
        footer = f"{index + 1} / {total}"
        return self.compose_hero(
            out_path,
            hook=title,
            body=body,
            footer=footer,
            layout=layout,
            palette=kwargs.get("palette"),
            bg_image=kwargs.get("bg_image"),
            creative_key=f"{title}-{index}",
            size=size,
            typography=kwargs.get("typography"),
        )

    # --- Layouts ----------------------------------------------------------------

    def _layout_magazine_cover(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        hook_f = self._font(int(46 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(26 * typo["body_mul"]))
        brand = self.brand_name.upper() if typo["brand_caps"] else self.brand_name

        draw.rectangle([0, 0, w, 100], fill=(255, 255, 255))
        draw.text((44, 32), brand, fill=(12, 12, 12), font=self._font(34, bold=True))
        draw.line([(44, 88), (w - 44, 88)], fill=accent, width=4)
        draw.text((w - 260, 38), "BRIEFING", fill=accent, font=self._font(22, bold=True))

        block_h = 340 if has_bg else 380
        draw.rectangle([0, h - block_h, w, h], fill=(252, 251, 248))
        y = h - block_h + 36
        y = self._draw_lines(draw, 44, y, hook, font=hook_f, fill=(18, 18, 18), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(52 * typo["hook_mul"]))
        y += 8
        self._draw_lines(draw, 44, y, body, font=body_f, fill=(70, 70, 80), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(34 * typo["body_mul"]))
        draw.text((44, h - 52), footer[:65], fill=accent, font=self._font(20, bold=True))

    def _layout_editorial_overlay(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        panel_h = 420 if has_bg else 480
        self._apply_scrim(base, self._scrim(size, direction="bottom", height=panel_h))
        base.paste(self._glass_panel(base, (40, h - panel_h + 20, w - 40, h - 36), alpha=178, radius=24))
        draw = ImageDraw.Draw(base)
        draw.rounded_rectangle([40, h - panel_h + 20, w - 40, h - 36], radius=24, outline=accent, width=2)
        draw.text((64, 48), self.brand_name.upper() if typo["brand_caps"] else self.brand_name, fill=accent, font=self._font(24, bold=True))
        hook_f = self._font(int(50 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(28 * typo["body_mul"]))
        y = h - panel_h + 56
        y = self._draw_lines(draw, 64, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(58 * typo["hook_mul"]))
        y += 10
        self._draw_lines(draw, 64, y, body, font=body_f, fill=(195, 205, 225), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(36 * typo["body_mul"]))
        draw.text((64, h - 72), footer[:70], fill=(150, 160, 180), font=self._font(20))

    def _layout_apple_keynote(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        if not has_bg:
            base.paste(Image.blend(base, Image.new("RGB", size, (5, 5, 8)), 0.35))
        self._apply_scrim(base, self._scrim(size, direction="full", height=h, max_alpha=140))
        draw = ImageDraw.Draw(base)
        hook_f = self._font(int(64 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(30 * typo["body_mul"]))
        hook_lines = wrap_text(hook, limits["hook_wrap"])[: limits["hook_lines"]]
        total_h = len(hook_lines) * int(72 * typo["hook_mul"]) + int(50 * typo["body_mul"])
        y = (h - total_h) // 2 - 20
        for line in hook_lines:
            bbox = draw.textbbox((0, 0), line, font=hook_f)
            tw = bbox[2] - bbox[0]
            draw.text(((w - tw) // 2, y), line, fill=(255, 255, 255), font=hook_f)
            y += int(72 * typo["hook_mul"])
        y += 16
        for line in wrap_text(body, limits["body_wrap"] + 6)[: limits["body_lines"]]:
            bbox = draw.textbbox((0, 0), line, font=body_f)
            tw = bbox[2] - bbox[0]
            draw.text(((w - tw) // 2, y), line, fill=(180, 185, 195), font=body_f)
            y += int(38 * typo["body_mul"])
        draw.text((w // 2 - 80, h - 64), footer[:40], fill=accent, font=self._font(20, bold=True))

    def _layout_pitch_deck(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        primary = colors[1] if len(colors) > 1 else self.primary
        draw.rectangle([0, 0, 14, h], fill=accent)
        draw.rectangle([14, 0, w, 120], fill=_blend_rgb(primary, (0, 0, 0), 0.15))
        draw.text((48, 42), self.brand_name.upper() if typo["brand_caps"] else self.brand_name, fill=(255, 255, 255), font=self._font(28, bold=True))
        draw.text((w - 120, 48), "SLIDE", fill=(200, 210, 230), font=self._font(18))
        base.paste(self._glass_panel(base, (48, 160, w - 48, h - 100), alpha=200, tint=(12, 16, 28), radius=20))
        draw = ImageDraw.Draw(base)
        hook_f = self._font(int(44 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(26 * typo["body_mul"]))
        y = 200
        y = self._draw_lines(draw, 80, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(52 * typo["hook_mul"]))
        y += 20
        for i, line in enumerate(wrap_text(body, limits["body_wrap"])[: limits["body_lines"]]):
            draw.ellipse([80, y + 8, 96, y + 24], fill=accent)
            draw.text((112, y), line, fill=(200, 210, 230), font=body_f)
            y += int(40 * typo["body_mul"])
        draw.text((80, h - 72), footer[:60], fill=(130, 140, 160), font=self._font(20))

    def _layout_bento_grid(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        c2 = colors[1] if len(colors) > 1 else self.primary
        if has_bg:
            self._apply_scrim(base, self._scrim(size, direction="full", height=h, max_alpha=100))
        draw = ImageDraw.Draw(base)
        gap = 16
        cells = [
            (gap, gap, w // 2 - gap // 2, h // 2 - gap // 2),
            (w // 2 + gap // 2, gap, w - gap, h // 2 - gap // 2),
            (gap, h // 2 + gap // 2, w // 3 - gap // 2, h - gap),
            (w // 3 + gap // 2, h // 2 + gap // 2, 2 * w // 3 - gap // 2, h - gap),
            (2 * w // 3 + gap // 2, h // 2 + gap // 2, w - gap, h - gap),
        ]
        fills = [accent, c2, (30, 35, 50), (40, 45, 62), accent]
        labels = ["AI", "DATA", "•", "•", "GO"]
        for box, fill, label in zip(cells, fills, labels):
            base.paste(self._glass_panel(base, box, alpha=190, tint=fill, radius=18))
            draw = ImageDraw.Draw(base)
            draw.rounded_rectangle(list(box), radius=18, outline=(255, 255, 255, 40), width=1)
            lx, ly = box[0] + 20, box[1] + 16
            draw.text((lx, ly), label, fill=(255, 255, 255), font=self._font(22, bold=True))
        main = cells[0]
        hook_f = self._font(int(36 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(22 * typo["body_mul"]))
        y = main[1] + 56
        y = self._draw_lines(draw, main[0] + 24, y, hook, font=hook_f, fill=(255, 255, 255), wrap=14, max_lines=limits["hook_lines"], line_height=int(42 * typo["hook_mul"]))
        self._draw_lines(draw, main[0] + 24, y + 8, body, font=body_f, fill=(210, 215, 230), wrap=16, max_lines=limits["body_lines"], line_height=int(28 * typo["body_mul"]))
        draw.text((gap + 8, h - 48), footer[:50], fill=(180, 190, 210), font=self._font(18))

    def _layout_hero_banner(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        primary = colors[1] if len(colors) > 1 else self.primary
        banner_y0 = int(h * 0.38)
        banner_y1 = int(h * 0.72)
        banner = Image.new("RGBA", size, (0, 0, 0, 0))
        bd = ImageDraw.Draw(banner)
        bd.rectangle([0, banner_y0, w, banner_y1], fill=(*primary, 215))
        bd.rectangle([0, banner_y0, w, banner_y0 + 6], fill=(*accent, 255))
        base.paste(Image.alpha_composite(base.convert("RGBA"), banner).convert("RGB"))
        draw = ImageDraw.Draw(base)
        draw.text((48, 48), self.brand_name.upper() if typo["brand_caps"] else self.brand_name, fill=(255, 255, 255), font=self._font(26, bold=True))
        hook_f = self._font(int(48 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(26 * typo["body_mul"]))
        y = banner_y0 + 36
        y = self._draw_lines(draw, 48, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(54 * typo["hook_mul"]))
        y += 8
        self._draw_lines(draw, 48, y, body, font=body_f, fill=(220, 225, 240), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(32 * typo["body_mul"]))
        draw.text((48, h - 56), footer[:60], fill=accent, font=self._font(20, bold=True))

    def _layout_infographic(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        c2 = colors[1] if len(colors) > 1 else self.primary
        if has_bg:
            self._apply_scrim(base, self._scrim(size, direction="top", height=200, max_alpha=180))
        draw = ImageDraw.Draw(base)
        draw.rectangle([0, 0, w, 88], fill=_blend_rgb(c2, (0, 0, 0), 0.2))
        draw.text((40, 28), "INSIGHT", fill=accent, font=self._font(24, bold=True))
        draw.text((w - 200, 30), self.brand_name[:20].upper(), fill=(255, 255, 255), font=self._font(20, bold=True))
        # Three stat nodes
        nodes = [(120, 200), (w // 2, 280), (w - 120, 200)]
        for i, (cx, cy) in enumerate(nodes):
            r = 56
            draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=accent if i == 1 else c2, outline=(255, 255, 255), width=2)
            draw.text((cx - 12, cy - 18), f"{i + 1}", fill=(255, 255, 255), font=self._font(32, bold=True))
            if i < len(nodes) - 1:
                nx = nodes[i + 1][0]
                draw.line([(cx + r, cy), (nx - r, nodes[i + 1][1])], fill=(255, 255, 255), width=3)
        base.paste(self._glass_panel(base, (40, 400, w - 40, h - 80), alpha=185, radius=22))
        draw = ImageDraw.Draw(base)
        hook_f = self._font(int(40 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(24 * typo["body_mul"]))
        y = 430
        y = self._draw_lines(draw, 64, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(48 * typo["hook_mul"]))
        self._draw_lines(draw, 64, y + 8, body, font=body_f, fill=(190, 200, 220), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(32 * typo["body_mul"]))
        draw.text((64, h - 56), footer[:55], fill=accent, font=self._font(18))

    def _layout_news_broadcast(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        live_red = (220, 38, 38)
        self._apply_scrim(base, self._scrim(size, direction="bottom", height=320, max_alpha=210))
        draw = ImageDraw.Draw(base)
        # Top ticker bar
        draw.rectangle([0, 0, w, 56], fill=(180, 20, 30))
        draw.text((20, 14), "BREAKING", fill=(255, 255, 255), font=self._font(24, bold=True))
        draw.rectangle([w - 160, 12, w - 20, 44], fill=(0, 0, 0))
        draw.ellipse([w - 150, 22, w - 136, 36], fill=live_red)
        draw.text((w - 128, 16), "LIVE", fill=(255, 255, 255), font=self._font(20, bold=True))
        # Lower third
        draw.rectangle([0, h - 240, w, h], fill=(12, 14, 22))
        draw.rectangle([0, h - 240, 8, h], fill=accent)
        hook_f = self._font(int(44 * typo["hook_mul"]), bold=True)
        body_f = self._font(int(26 * typo["body_mul"]))
        y = h - 210
        y = self._draw_lines(draw, 32, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(50 * typo["hook_mul"]))
        self._draw_lines(draw, 32, y + 6, body, font=body_f, fill=(180, 190, 210), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(32 * typo["body_mul"]))
        draw.text((32, h - 48), f"{self.brand_name}  |  {footer[:45]}", fill=accent, font=self._font(18, bold=True))

    def _layout_timeline(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        if has_bg:
            self._apply_scrim(base, self._scrim(size, direction="full", height=h, max_alpha=90))
        draw = ImageDraw.Draw(base)
        draw.text((48, 40), self.brand_name.upper() if typo["brand_caps"] else self.brand_name, fill=accent, font=self._font(24, bold=True))
        line_x = 80
        draw.line([(line_x, 120), (line_x, h - 100)], fill=(255, 255, 255), width=4)
        steps = [
            (140, hook, True),
            (340, body, False),
            (520, footer, False),
        ]
        hook_f = self._font(int(38 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(24 * typo["body_mul"]))
        for y_pos, text, is_hook in steps:
            draw.ellipse([line_x - 12, y_pos - 12, line_x + 12, y_pos + 12], fill=accent, outline=(255, 255, 255), width=2)
            font = hook_f if is_hook else body_f
            wrap = limits["hook_wrap"] if is_hook else limits["body_wrap"]
            max_l = limits["hook_lines"] if is_hook else limits["body_lines"]
            tx = line_x + 36
            for i, line in enumerate(wrap_text(text, wrap)[:max_l]):
                draw.text((tx, y_pos - 8 + i * int(34 * typo["body_mul"])), line, fill=(255, 255, 255) if is_hook else (190, 200, 220), font=font)

    def _layout_dashboard_ui(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        c2 = colors[1] if len(colors) > 1 else self.primary
        base.paste(Image.blend(base, Image.new("RGB", size, (14, 16, 24)), 0.45 if has_bg else 0.15))
        draw = ImageDraw.Draw(base)
        # Sidebar
        draw.rectangle([0, 0, 200, h], fill=(18, 20, 32))
        for i, label in enumerate(["Home", "Metrics", "Feed"]):
            draw.rounded_rectangle([20, 80 + i * 56, 180, 120 + i * 56], radius=10, fill=c2 if i == 1 else (28, 32, 48))
            draw.text((36, 92 + i * 56), label, fill=(220, 225, 240), font=self._font(18))
        # Metric cards
        cards = [(230, 40, 520, 180), (540, 40, w - 30, 180), (230, 200, 520, 340), (540, 200, w - 30, 340)]
        for i, box in enumerate(cards):
            base.paste(self._glass_panel(base, box, alpha=170, tint=(22, 26, 40), radius=14))
            draw = ImageDraw.Draw(base)
            draw.text((box[0] + 16, box[1] + 14), f"Metric {i + 1}", fill=(140, 150, 170), font=self._font(16))
            # Mini bar chart
            for j in range(5):
                bh = rng.randint(20, 70)
                bx = box[0] + 20 + j * 28
                draw.rectangle([bx, box[3] - 20 - bh, bx + 18, box[3] - 20], fill=accent if j == 2 else c2)
        # Main content panel
        base.paste(self._glass_panel(base, (230, 370, w - 30, h - 40), alpha=195, radius=16))
        draw = ImageDraw.Draw(base)
        hook_f = self._font(int(34 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(22 * typo["body_mul"]))
        y = 400
        y = self._draw_lines(draw, 260, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(42 * typo["hook_mul"]))
        self._draw_lines(draw, 260, y + 6, body, font=body_f, fill=(170, 180, 200), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(28 * typo["body_mul"]))
        draw.text((260, h - 68), footer[:50], fill=accent, font=self._font(16))

    def _layout_split_comparison(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        c2 = colors[1] if len(colors) > 1 else self.primary
        mid = w // 2
        left_panel = Image.new("RGBA", size, (0, 0, 0, 0))
        lp = ImageDraw.Draw(left_panel)
        lp.rectangle([0, 0, mid, h], fill=(*_blend_rgb(c2, (0, 0, 0), 0.35), 200))
        right_panel = Image.new("RGBA", size, (0, 0, 0, 0))
        rp = ImageDraw.Draw(right_panel)
        rp.rectangle([mid, 0, w, h], fill=(*_blend_rgb(accent, (0, 0, 0), 0.45), 200))
        base.paste(Image.alpha_composite(base.convert("RGBA"), left_panel).convert("RGB"))
        base.paste(Image.alpha_composite(base.convert("RGBA"), right_panel).convert("RGB"))
        draw = ImageDraw.Draw(base)
        draw.rectangle([mid - 36, h // 2 - 36, mid + 36, h // 2 + 36], fill=(255, 255, 255))
        draw.text((mid - 22, h // 2 - 20), "VS", fill=(20, 20, 30), font=self._font(28, bold=True))
        hook_f = self._font(int(32 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(20 * typo["body_mul"]))
        left_text = wrap_text(hook, 12)[:2]
        for i, line in enumerate(left_text):
            draw.text((32, 180 + i * 40), line, fill=(255, 255, 255), font=hook_f)
        right_text = wrap_text(body, 12)[: limits["body_lines"]]
        for i, line in enumerate(right_text):
            draw.text((mid + 32, 180 + i * 34), line, fill=(255, 255, 255), font=body_f)
        draw.text((32, h - 64), "BEFORE", fill=(200, 210, 230), font=self._font(18, bold=True))
        draw.text((mid + 32, h - 64), "AFTER", fill=(200, 210, 230), font=self._font(18, bold=True))
        draw.text((w // 2 - 100, h - 36), footer[:40], fill=accent, font=self._font(16))

    def _layout_glass_card(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        pad = 70 if has_bg else 90
        base.paste(self._glass_panel(base, (pad, 140, w - pad, h - 140), alpha=188, radius=32))
        draw = ImageDraw.Draw(base)
        draw.rounded_rectangle([pad, 140, w - pad, h - 140], radius=32, outline=accent, width=3)
        draw.rounded_rectangle([pad + 36, 180, pad + 220, 228], radius=16, fill=accent)
        draw.text((pad + 52, 192), "UPDATE", fill=(15, 20, 35), font=self._font(22, bold=True))
        hook_f = self._font(int(46 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(26 * typo["body_mul"]))
        y = 260
        y = self._draw_lines(draw, pad + 40, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(54 * typo["hook_mul"]))
        y += 12
        self._draw_lines(draw, pad + 40, y, body, font=body_f, fill=(195, 205, 225), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(34 * typo["body_mul"]))
        draw.text((pad + 40, h - 180), footer[:55], fill=(140, 150, 170), font=self._font(20))

    def _layout_minimal_premium(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        if not has_bg:
            base.paste(Image.blend(base, Image.new("RGB", size, (8, 8, 10)), 0.25))
        draw = ImageDraw.Draw(base)
        draw.rectangle([72, 72, w - 72, h - 72], outline=(80, 80, 90), width=1)
        draw.line([(72, 140), (220, 140)], fill=accent, width=2)
        brand = self.brand_name.upper() if typo["brand_caps"] else self.brand_name
        draw.text((72, 96), brand, fill=accent, font=self._font(20, bold=False))
        hook_f = self._font(int(52 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(26 * typo["body_mul"]))
        y = 180
        y = self._draw_lines(draw, 72, y, hook, font=hook_f, fill=(245, 245, 248), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(58 * typo["hook_mul"]))
        y += 24
        self._draw_lines(draw, 72, y, body, font=body_f, fill=(160, 165, 175), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(36 * typo["body_mul"]))
        draw.text((72, h - 100), footer[:60], fill=(100, 105, 115), font=self._font(18))

    def _layout_neon_tech(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        overlay = Image.new("RGB", size, (5, 8, 16))
        base.paste(Image.blend(base, overlay, 0.58))
        draw = ImageDraw.Draw(base)
        m = 32
        for offset, width in [(0, 4), (10, 1)]:
            draw.rounded_rectangle([m + offset, m + offset, w - m - offset, h - m - offset], radius=22, outline=accent, width=width)
        draw.text((56, 56), "◈ TECH SIGNAL", fill=accent, font=self._font(26, bold=True))
        hook_f = self._font(int(54 * typo["hook_mul"]), bold=True)
        body_f = self._font(int(28 * typo["body_mul"]))
        y = 180
        y = self._draw_lines(draw, 56, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(62 * typo["hook_mul"]))
        draw.rectangle([56, y + 8, 200, y + 14], fill=accent)
        y += 28
        self._draw_lines(draw, 56, y, body, font=body_f, fill=(180, 200, 255), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(36 * typo["body_mul"]))
        draw.text((56, h - 72), footer[:55], fill=(130, 150, 200), font=self._font(20))

    def _layout_bold_stat(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        stat = (body.split(".")[0][:20] if body else hook.split()[0][:12]).upper()
        if has_bg:
            base.paste(Image.blend(base, Image.new("RGB", size, (6, 8, 14)), 0.5))
        else:
            base.paste(Image.new("RGB", size, (8, 10, 18)))
        draw = ImageDraw.Draw(base)
        for i in range(0, w, 54):
            draw.line([(i, 0), (i, h)], fill=(22, 26, 40), width=1)
        for i in range(0, h, 54):
            draw.line([(0, i), (w, i)], fill=(22, 26, 40), width=1)
        draw.ellipse([w - 400, -60, w + 60, 400], outline=accent, width=6)
        draw.text((64, 64), self.brand_name.upper() if typo["brand_caps"] else self.brand_name, fill=accent, font=self._font(24, bold=True))
        stat_line = wrap_text(stat, 10)[0] if stat else "NOW"
        draw.text((64, 170), stat_line, fill=accent, font=self._font(int(88 * typo["hook_mul"]), bold=True))
        hook_f = self._font(int(40 * typo["hook_mul"]), bold=True)
        body_f = self._font(int(26 * typo["body_mul"]))
        y = 340
        y = self._draw_lines(draw, 64, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(48 * typo["hook_mul"]))
        self._draw_lines(draw, 64, y + 12, body, font=body_f, fill=(170, 180, 200), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(32 * typo["body_mul"]))
        draw.text((64, h - 72), footer[:60], fill=(110, 120, 140), font=self._font(20))

    def _layout_gradient_story(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        self._apply_scrim(base, self._scrim(size, direction="bottom", height=520, max_alpha=235))
        draw = ImageDraw.Draw(base)
        draw.ellipse([w - 200, 48, w - 48, 200], fill=accent)
        draw.text((w - 165, 98), "01", fill=(12, 12, 20), font=self._font(44, bold=True))
        draw.text((48, 52), self.brand_name.upper() if typo["brand_caps"] else self.brand_name, fill=(255, 255, 255), font=self._font(22, bold=True))
        hook_f = self._font(int(56 * typo["hook_mul"]), bold=True)
        body_f = self._font(int(28 * typo["body_mul"]))
        y = h - 400
        y = self._draw_lines(draw, 48, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(64 * typo["hook_mul"]))
        self._draw_lines(draw, 48, y + 8, body, font=body_f, fill=(205, 210, 225), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(36 * typo["body_mul"]))
        draw.text((48, h - 60), f"{self.brand_name} · {footer[:38]}", fill=accent, font=self._font(22, bold=True))

    def _layout_corporate_linkedin(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        primary = colors[1] if len(colors) > 1 else self.primary
        draw.rectangle([0, 0, w, 130], fill=primary)
        draw.rectangle([0, 130, w, 136], fill=accent)
        draw = ImageDraw.Draw(base)
        draw.ellipse([48, 36, 108, 96], fill=(255, 255, 255))
        draw.text((58, 52), self.brand_name[:1].upper(), fill=primary, font=self._font(32, bold=True))
        draw.text((130, 48), self.brand_name, fill=(255, 255, 255), font=self._font(30, bold=True))
        draw.text((130, 88), "Industry Insights", fill=(220, 225, 235), font=self._font(18))
        content_y = 180 if has_bg else 170
        base.paste(self._glass_panel(base, (48, content_y, w - 48, h - 80), alpha=210, tint=(250, 251, 253), radius=12))
        draw = ImageDraw.Draw(base)
        hook_f = self._font(int(38 * typo["hook_mul"]), bold=typo["hook_bold"])
        body_f = self._font(int(26 * typo["body_mul"]))
        y = content_y + 36
        y = self._draw_lines(draw, 72, y, hook, font=hook_f, fill=(20, 25, 35), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(46 * typo["hook_mul"]))
        y += 12
        self._draw_lines(draw, 72, y, body, font=body_f, fill=(60, 70, 85), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(34 * typo["body_mul"]))
        draw.text((72, h - 100), footer[:65], fill=accent, font=self._font(18, bold=True))

    def _layout_cyberpunk(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        magenta = colors[2] if len(colors) > 2 else (255, 0, 128)
        cyan = colors[1] if len(colors) > 1 else (0, 255, 255)
        base.paste(Image.blend(base, Image.new("RGB", size, (10, 5, 20)), 0.5))
        draw = ImageDraw.Draw(base)
        # Scanlines
        for y in range(0, h, 4):
            draw.line([(0, y), (w, y)], fill=(0, 0, 0), width=1)
        # Glitch bars
        for _ in range(8):
            gy = rng.randint(0, h - 20)
            gh = rng.randint(2, 12)
            gx = rng.randint(-40, 40)
            draw.rectangle([gx, gy, w + gx, gy + gh], fill=magenta if rng.random() > 0.5 else cyan)
        draw.polygon([(0, 0), (w, 0), (w - 80, 100), (0, 140)], fill=_blend_rgb(magenta, (0, 0, 0), 0.5))
        draw.polygon([(w, h), (0, h), (100, h - 120), (w, h - 80)], fill=_blend_rgb(cyan, (0, 0, 0), 0.55))
        draw = ImageDraw.Draw(base)
        draw.text((48, 48), "▸ CYBER", fill=cyan, font=self._font(24, bold=True))
        draw.text((170, 48), self.brand_name.upper(), fill=magenta, font=self._font(24, bold=True))
        hook_f = self._font(int(48 * typo["hook_mul"]), bold=True)
        body_f = self._font(int(26 * typo["body_mul"]))
        y = 200
        for i, line in enumerate(wrap_text(hook, limits["hook_wrap"])[: limits["hook_lines"]]):
            offset = 3 if i % 2 else 0
            draw.text((52 + offset, y), line, fill=cyan, font=hook_f)
            draw.text((48, y), line, fill=(255, 255, 255), font=hook_f)
            y += int(56 * typo["hook_mul"])
        y += 12
        self._draw_lines(draw, 48, y, body, font=body_f, fill=(200, 180, 220), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(34 * typo["body_mul"]))
        draw.text((48, h - 64), footer[:55], fill=accent, font=self._font(18, bold=True))

    def _layout_split_diagonal(self, draw, base, *, hook, body, footer, colors, rng, size, typo, limits, has_bg):
        w, h = size
        accent = colors[-1]
        panel = Image.new("RGBA", size, (0, 0, 0, 0))
        pd = ImageDraw.Draw(panel)
        pd.polygon([(0, int(h * 0.32)), (w, int(h * 0.12)), (w, h), (0, h)], fill=(8, 12, 24, 215))
        base.paste(Image.alpha_composite(base.convert("RGBA"), panel).convert("RGB"))
        draw = ImageDraw.Draw(base)
        draw.rectangle([56, 44, 56 + 120, 50], fill=accent)
        draw.text((56, 64), self.brand_name.upper() if typo["brand_caps"] else self.brand_name, fill=(255, 255, 255), font=self._font(22, bold=True))
        hook_f = self._font(int(54 * typo["hook_mul"]), bold=True)
        body_f = self._font(int(28 * typo["body_mul"]))
        y = int(h * 0.40)
        y = self._draw_lines(draw, 56, y, hook, font=hook_f, fill=(255, 255, 255), wrap=limits["hook_wrap"], max_lines=limits["hook_lines"], line_height=int(62 * typo["hook_mul"]))
        y += 12
        self._draw_lines(draw, 56, y, body, font=body_f, fill=(185, 195, 215), wrap=limits["body_wrap"], max_lines=limits["body_lines"], line_height=int(36 * typo["body_mul"]))
        draw.text((56, h - 64), f"{self.brand_name}  ·  {footer[:38]}", fill=accent, font=self._font(22, bold=True))
