"""Post-processes AI-generated SVG: strips unsafe tags, and auto-centers/auto-fits
onto the specified square canvas. Applies background colors."""

from __future__ import annotations
import io
import math
import re
import xml.etree.ElementTree as ET

NUM_RE = re.compile(r"-?\d+\.?\d*(?:[eE][-+]?\d+)?")
# text এবং tspan ট্যাগকেও যুক্ত করা হলো, যাতে এআই নরমাল ফন্ট দিতে না পারে!
# style ট্যাগও বাদ দেওয়া হলো — এটা <svg> এর ভেতরে থাকলেও ব্রাউজারে পুরো পেজ জুড়ে
# CSS হিসেবে কাজ করে (scope হয় না), যার কারণে গ্রিডের ডিলিট বাটন জায়গা থেকে সরে
# বাইরে চলে যাচ্ছিল।
DANGEROUS_TAGS = {"script", "foreignobject", "image", "iframe", "text", "tspan", "style"}
SVG_NS = "http://www.w3.org/2000/svg"

# How much empty padding we keep around the design when auto-fitting.
FIT_PADDING = 60.0

def _parse_svg(svg_string: str):
    root = ET.fromstring(svg_string.strip())
    for el in root.iter():
        if "}" in el.tag: el.tag = el.tag.split("}", 1)[1]
        for attr in list(el.attrib):
            if attr.startswith("{"):
                local = attr.split("}", 1)[1]
                el.attrib[local] = el.attrib.pop(attr)
    return root

def _serialize_svg(root, width: int = 800, height: int = 800) -> str:
    root.set("xmlns", SVG_NS)
    # Canvas size নিশ্চিত করা হলো
    root.set("width", str(width))
    root.set("height", str(height))
    return ET.tostring(root, encoding="unicode")

def _bbox_for(el) -> tuple | None:
    """Rough geometry-only bbox for a single element (ignores ancestor transforms)."""
    tag = el.tag.lower()
    g = el.get
    try:
        if tag == "rect":
            x, y, w, h = float(g("x", 0)), float(g("y", 0)), float(g("width", 0)), float(g("height", 0))
            return (x, y, x + w, y + h)
        if tag == "circle":
            cx, cy, r = float(g("cx", 0)), float(g("cy", 0)), float(g("r", 0))
            return (cx - r, cy - r, cx + r, cy + r)
        if tag == "ellipse":
            cx, cy = float(g("cx", 0)), float(g("cy", 0))
            rx, ry = float(g("rx", 0)), float(g("ry", 0))
            return (cx - rx, cy - ry, cx + rx, cy + ry)
        if tag == "line":
            x1, y1 = float(g("x1", 0)), float(g("y1", 0))
            x2, y2 = float(g("x2", 0)), float(g("y2", 0))
            return (min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2))
        if tag in ("polygon", "polyline"):
            nums = [float(n) for n in NUM_RE.findall(g("points", ""))]
            xs, ys = nums[0::2], nums[1::2]
            if xs and ys: return (min(xs), min(ys), max(xs), max(ys))
        if tag == "path":
            nums = [float(n) for n in NUM_RE.findall(g("d", ""))]
            xs, ys = nums[0::2], nums[1::2]
            if xs and ys: return (min(xs), min(ys), max(xs), max(ys))
    except Exception: pass
    return None

# Tags whose contents are NOT directly rendered on the canvas
NON_VISUAL_CONTAINERS = {"defs", "clippath", "mask", "symbol", "pattern"}

def _iter_visual(el):
    """Yield every descendant of el, skipping whole subtrees rooted at a non-visual container."""
    for child in list(el):
        tag = child.tag.lower()
        if tag in NON_VISUAL_CONTAINERS or tag in ("style", "title", "desc"):
            continue
        yield child
        yield from _iter_visual(child)

def _is_bg_rect(el) -> bool:
    return el.tag.lower() == "rect" and el.get("data-role") == "bg"

def _geometry_bbox(elements) -> tuple | None:
    """Fallback bbox: raw coordinate math, ignoring any transforms."""
    minx = miny = float("inf")
    maxx = maxy = float("-inf")
    for top in elements:
        candidates = [top] if top.tag.lower() not in ("g", "svg") else list(_iter_visual_including_self(top))
        for el in candidates:
            if _is_bg_rect(el): continue
            box = _bbox_for(el)
            if box:
                x0, y0, x1, y1 = box
                minx, miny = min(minx, x0), min(miny, y0)
                maxx, maxy = max(maxx, x1), max(maxy, y1)
    if minx < maxx and miny < maxy:
        return (minx, miny, maxx, maxy)
    return None

def _iter_visual_including_self(el):
    yield el
    yield from _iter_visual(el)

def _pixel_bbox(defs_el, movable_elements, canvas_size: int):
    """Render just the movable content to a raster and measure the real visible bounding box."""
    try:
        import cairosvg
        from PIL import Image
    except Exception:
        return None

    probe = ET.Element("svg", {"viewBox": f"0 0 {canvas_size} {canvas_size}", "xmlns": SVG_NS})
    if defs_el is not None:
        probe.append(_clone(defs_el))
    for el in movable_elements:
        if _is_bg_rect(el): continue
        probe.append(_clone(el))

    try:
        svg_bytes = ET.tostring(probe, encoding="unicode").encode("utf-8")
        png_bytes = cairosvg.svg2png(bytestring=svg_bytes, output_width=canvas_size, background_color="white")
        img = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
        alpha = img.split()[-1]
        bbox = alpha.getbbox()
        if not bbox:
            return None
        x0, y0, x1, y1 = bbox
        return (float(x0), float(y0), float(x1), float(y1))
    except Exception:
        return None

def _clone(el):
    """Deep-copy an ElementTree element."""
    new_el = ET.Element(el.tag, dict(el.attrib))
    new_el.text, new_el.tail = el.text, el.tail
    for child in list(el):
        new_el.append(_clone(child))
    return new_el

def _group_and_center(root, canvas_size: int):
    """Center and auto-fit the design on the square canvas."""
    keep_direct = {"defs", "title", "desc", "style"} | NON_VISUAL_CONTAINERS
    defs_el = next((c for c in list(root) if c.tag.lower() == "defs"), None)
    movable = [c for c in list(root) if c.tag.lower() not in keep_direct and c.get("data-role") != "bg"]
    if not movable: return

    # Try pixel-based measurement first, fall back to geometry math
    bbox = _pixel_bbox(defs_el, movable, canvas_size)
    if bbox is None:
        bbox = _geometry_bbox(movable)

    dx = dy = 0.0
    scale = 1.0
    if bbox:
        minx, miny, maxx, maxy = bbox
        width, height = maxx - minx, maxy - miny
        if width > 0 and height > 0:
            fit_target = canvas_size - 2 * FIT_PADDING
            longest = max(width, height)
            if longest > fit_target:
                scale = fit_target / longest
            content_cx, content_cy = (minx + maxx) / 2, (miny + maxy) / 2
            dx = canvas_size / 2 - scale * content_cx
            dy = canvas_size / 2 - scale * content_cy

    for c in movable: root.remove(c)

    group = ET.Element("g", {"id": "glass-element"})
    transform_parts = []
    if abs(dx) >= 0.5 or abs(dy) >= 0.5:
        transform_parts.append(f"translate({dx:.2f},{dy:.2f})")
    if abs(scale - 1.0) >= 0.01:
        transform_parts.append(f"scale({scale:.4f})")
    if transform_parts:
        group.set("transform", " ".join(transform_parts))

    for c in movable: group.append(c)
    root.append(group)

def _strip_dangerous(root):
    """Remove unsafe tags"""
    parent_map = {c: p for p in root.iter() for c in p}
    for el in list(root.iter()):
        if el.tag.lower() in DANGEROUS_TAGS:
            parent = parent_map.get(el)
            if parent is not None: parent.remove(el)

def clean_svg(svg_string: str, width: int = 800, height: int = 800, auto_fit: bool = True) -> str:
    """Clean SVG: strip dangerous tags, auto-center/auto-fit to specified canvas size."""
    try:
        root = _parse_svg(svg_string)
        root.set("viewBox", f"0 0 {int(width)} {int(height)}")
        _strip_dangerous(root)
        if auto_fit and width == height:  # Only center for square canvas
            _group_and_center(root, width)
        return _serialize_svg(root, width, height)
    except Exception:
        return svg_string

def apply_background(svg_string: str, bg_color: str | None, width: int = 800, height: int = 800) -> str:
    """Apply background color to SVG"""
    if not bg_color or bg_color in ("auto", "transparent", "none"):
        return svg_string
    
    try:
        root = _parse_svg(svg_string)
        # Remove any existing background rect
        for child in list(root):
            if child.get("data-role") == "bg": root.remove(child)

        # Add new background rect at the beginning
        rect = ET.Element("rect", {
            "x": "0", 
            "y": "0", 
            "width": str(width), 
            "height": str(height), 
            "fill": bg_color, 
            "data-role": "bg"
        })
        root.insert(0, rect)
        return _serialize_svg(root, width, height)
    except Exception:
        return svg_string
