"""
Banner image generator for the macOS Security Compliance Project (mSCP).

Produces per-platform banner images (macOS, iOS, visionOS) in light, dark,
and high-contrast (8 px stroke) modes by compositing the mSCP logo with
platform-specific text and a rounded colored divider.

Background removal uses a BFS flood-fill from image edges (``isolate_logo``).
Logo pixels are recolored per platform via ``change_color``.

Public entry point: ``generate_mscp_banners()``

Configuration keys consumed from ``config``:
    images_dir  — directory containing ``mscp_logo.png``; output is written here.

Output filename pattern:
    mscp_banner_<platform>_<mode>.png
    where platform ∈ {macos, ios, visionos} and mode ∈ {light, dark, both_8px}.
"""

import argparse
from collections import deque
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont
from yaspin.core import Yaspin
from yaspin.spinners import Spinners

from ..common_utils import (
    config,
    logger,
    conditional_inject_spinner,
)

LIGHT = {"fill": (0, 0, 0, 255), "stroke_fill": None, "stroke_width": 0}
DARK = {"fill": (255, 255, 255, 255), "stroke_fill": None, "stroke_width": 0}
BOTH_8PX = {
    "fill": (255, 255, 255, 255),
    "stroke_fill": (0, 0, 0, 255),
    "stroke_width": 8,
}

MSCP_GREEN = (41, 99, 54, 255)
PLATFORMS = ["macOS", "iOS", "visionOS"]
PLATFORM_COLORS = {
    "macOS": (41, 99, 54, 255),
    "iOS": (88, 86, 214, 255),
    "visionOS": (255, 45, 85, 255),
}

SUFFIX = "Security Configuration Guide"
SUBTITLE = "as generated by the macOS Security Compliance Project"
MODES = {"light": LIGHT, "dark": DARK, "both_8px": BOTH_8PX}
FONT = "HelveticaNeue.ttc"

BANNER_HEIGHT = 420
LOGO_LEFT_MARGIN = 110
LOGO_RIGHT_PADDING = 70
DIVIDER_GAP_LEFT = 25
DIVIDER_GAP_RIGHT = 25
DIVIDER_WIDTH = 8
DIVIDER_HEIGHT = 92
GAP_BETWEEN = 22
CANVAS_RIGHT_PADDING = 40


def color_dist(a: tuple, b: tuple) -> int:
    return sum(abs(a[i] - b[i]) for i in range(3))


def isolate_logo(logo_path: str, threshold: int = 40) -> Image.Image:
    """Remove the background of a logo image using a flood-fill from the edges.

    Assumes the top-left pixel is representative of the background color.
    Edge-reachable pixels within *threshold* color distance (sum of absolute
    RGB channel differences) of that background are made fully transparent.
    The result is cropped to the tightest bounding box of remaining opaque content.

    Args:
        logo_path: Path to the source image (any PIL-supported format).
        threshold: Maximum RGB channel distance to treat a pixel as background.

    Returns:
        RGBA Image with background removed and cropped to content bounds.
    """
    img = Image.open(logo_path).convert("RGBA")
    w, h = img.size
    src_pixels = img.load()
    bg = img.getpixel((0, 0))  # top-left pixel assumed to be background

    # visited is indexed [x][y] to match PIL's (x, y) convention
    visited = [[False] * h for _ in range(w)]
    q = deque()

    for x in range(w):
        for y in (0, h - 1):
            if not visited[x][y] and color_dist(src_pixels[x, y], bg) <= threshold:
                visited[x][y] = True
                q.append((x, y))
    for y in range(h):
        for x in (0, w - 1):
            if not visited[x][y] and color_dist(src_pixels[x, y], bg) <= threshold:
                visited[x][y] = True
                q.append((x, y))

    while q:
        x, y = q.popleft()
        for nx, ny in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
            if 0 <= nx < w and 0 <= ny < h and not visited[nx][ny]:
                if color_dist(src_pixels[nx, ny], bg) <= threshold:
                    visited[nx][ny] = True
                    q.append((nx, ny))

    out = img.copy()
    out_pixels = out.load()
    for x in range(w):
        for y in range(h):
            if visited[x][y]:
                out_pixels[x, y] = (0, 0, 0, 0)

    bbox = out.getchannel("A").getbbox()
    return out.crop(bbox)


def font(
    font_path: str, size: int, bold: bool = False, italic: bool = False
) -> ImageFont.FreeTypeFont:
    # TTC index: 0=regular, 1=bold, 2=italic
    index = 2 if italic else (1 if bold else 0)
    return ImageFont.truetype(font_path, size, index=index)


def draw_text(
    draw: ImageDraw.ImageDraw,
    xy: tuple,
    text: str,
    fnt: ImageFont.FreeTypeFont,
    style: dict,
) -> None:
    kwargs = {"font": fnt, "fill": style["fill"]}
    if style.get("stroke_fill") is not None and style.get("stroke_width", 0) > 0:
        kwargs["stroke_fill"] = style["stroke_fill"]
        kwargs["stroke_width"] = style["stroke_width"]
    draw.text(xy, text, **kwargs)


def change_color(image: Image.Image, old_color: tuple, new_color: tuple, tolerance: int = 0) -> Image.Image:
    pixels = image.load()
    width, height = image.size

    for y in range(height):
        for x in range(width):
            r, g, b, a = pixels[x, y]
            if a == 0:
                continue
            if (
                abs(r - old_color[0]) <= tolerance
                and abs(g - old_color[1]) <= tolerance
                and abs(b - old_color[2]) <= tolerance
            ):
                pixels[x, y] = new_color
    return image


@conditional_inject_spinner()
def generate_mscp_banners(sp: Yaspin, args: argparse.Namespace) -> None:
    """Generate MSCP banner images for all supported platforms and display modes.

    Reads ``mscp_logo.png`` from ``config["images_dir"]``, removes its background,
    recolors it per platform, and composites it with title text onto a transparent
    RGBA canvas.  Nine files are written in total (3 platforms × 3 modes).

    Args:
        sp: Yaspin spinner instance injected by ``@conditional_inject_spinner``.
        args: Parsed CLI arguments from argparse.
    """
    logger.info("Generating MSCP banners...")
    sp.spinner = Spinners.dots
    sp.text = "Generating MSCP banners"

    outdir = Path(config["images_dir"])
    logo = isolate_logo(Path(config["images_dir"]) / "mscp_logo.png")

    platform_font = font(FONT, 72, bold=True)
    suffix_font = font(FONT, 62)
    subtitle_font = font(FONT, 24, italic=True)

    probe = Image.new("RGBA", (10, 10), (0, 0, 0, 0))
    dp = ImageDraw.Draw(probe)

    platform_widths = {}
    platform_heights = {}
    max_platform_h = 0
    for p in PLATFORMS:
        bb = dp.textbbox((0, 0), p, font=platform_font)
        platform_widths[p] = bb[2] - bb[0]
        platform_heights[p] = bb[3] - bb[1]
        max_platform_h = max(max_platform_h, platform_heights[p])

    suffix_bb = dp.textbbox((0, 0), SUFFIX, font=suffix_font)
    suffix_h = suffix_bb[3] - suffix_bb[1]
    suffix_w = suffix_bb[2] - suffix_bb[0]

    block_h = max_platform_h + GAP_BETWEEN + suffix_h

    logo_w, logo_h = logo.size
    resized_logo_h = BANNER_HEIGHT - 40
    resized_logo_w = int(logo_w * (resized_logo_h / logo_h))
    logo_resized = logo.resize((resized_logo_w, resized_logo_h), Image.LANCZOS)

    logo_x = LOGO_LEFT_MARGIN
    logo_y = (BANNER_HEIGHT - logo_resized.height) // 2
    text_left = logo_x + logo_resized.width + LOGO_RIGHT_PADDING
    top_y = logo_y + (logo_resized.height - block_h) // 2 + 10

    text_block_h = max(max(platform_heights.values()), suffix_h)
    platform_y = (BANNER_HEIGHT - text_block_h) // 2 - 6
    suffix_y = platform_y + 8

    divider_y = (BANNER_HEIGHT - DIVIDER_HEIGHT) // 2 + 8

    count = 0
    for p in PLATFORMS:
        logger.info(f"Generating banner for {p}...")
        platform_w = platform_widths[p]

        divider_x = text_left + platform_w + DIVIDER_GAP_LEFT
        suffix_x = divider_x + DIVIDER_GAP_RIGHT + DIVIDER_WIDTH

        colored_logo = change_color(
            logo_resized.copy(),
            MSCP_GREEN[:3],
            PLATFORM_COLORS.get(p, MSCP_GREEN[:3]),
            tolerance=25,
        )

        canvas_width = (
            text_left
            + platform_w
            + DIVIDER_GAP_LEFT
            + DIVIDER_WIDTH
            + DIVIDER_GAP_RIGHT
            + suffix_w
            + CANVAS_RIGHT_PADDING
        )

        for mode_name, style in MODES.items():
            canvas = Image.new("RGBA", (canvas_width, BANNER_HEIGHT), (0, 0, 0, 0))
            draw = ImageDraw.Draw(canvas)
            canvas.alpha_composite(colored_logo, (logo_x, logo_y))
            draw_text(draw, (text_left, platform_y), p, platform_font, style)
            draw.rounded_rectangle(
                (divider_x, divider_y, divider_x + DIVIDER_WIDTH, divider_y + DIVIDER_HEIGHT),
                fill=PLATFORM_COLORS.get(p, MSCP_GREEN),
                radius=DIVIDER_WIDTH // 2,
            )
            draw_text(draw, (suffix_x, suffix_y), SUFFIX, suffix_font, style)
            draw_text(
                draw,
                (text_left, top_y + DIVIDER_HEIGHT + GAP_BETWEEN),
                SUBTITLE,
                subtitle_font,
                style,
            )
            canvas.save(outdir / f"mscp_banner_{p.lower()}_{mode_name}.png")
            count += 1

    logger.info(f"Created {count} files in {outdir}")
    sp.text = f"Created {count} files in {outdir}"
    sp.ok("✔")
