"""
AI Room Designer — QC / recognisability scoring (Stage 2, v2).

Stage 1 scored raw edge correlation, which mis-scores high-aggression modes:
when a mode replaces furniture BY DESIGN, furniture edges vanish and the raw
score reads "room rebuilt" even when the shell is untouched.

v2 adds an ARCHITECTURAL MASK: from the BEFORE photo we extract the edges that
belong to long straight structural lines (wall/ceiling junctions, window and
door outlines, floor footprint lines) via long-run line detection, dilate that
mask, and score edge survival ONLY inside it. Furniture edges outside the mask
are allowed to change freely.

Signals:
  edge_corr        — legacy raw edge correlation (kept for comparison)
  layout_corr      — blurred luminance correlation (big layout swaps)
  arch_edge_corr   — edge correlation inside the architectural mask (v2 hero)
  arch_survival    — share of before-arch edge energy with a matching after
                     edge inside the mask

Gate (v2): PASS requires arch_edge_corr >= 0.45 AND layout_corr >= 0.80.
That gate is mode-agnostic because the mask excludes furniture by construction.
"""
import numpy as np
from PIL import Image, ImageFilter

SIZE = 256
LINE_LEN = 41          # min run length (px at 256) to count as structural
LINE_THR = 0.35        # fraction of window that must be edge to count as a line


def _gray(path):
    img = Image.open(path).convert("L").resize((SIZE, SIZE))
    return np.asarray(img, dtype=np.float64)


def _edges(gray):
    gx = np.zeros_like(gray)
    gy = np.zeros_like(gray)
    gx[:, 1:-1] = gray[:, 2:] - gray[:, :-2]
    gy[1:-1, :] = gray[2:, :] - gray[:-2, :]
    return np.hypot(gx, gy)


def _corr(a, b):
    a = a.ravel() - a.mean()
    b = b.ravel() - b.mean()
    denom = np.linalg.norm(a) * np.linalg.norm(b)
    return float(a @ b / denom) if denom else 0.0


def _blur(path):
    img = Image.open(path).convert("L").resize((SIZE, SIZE))
    img = img.filter(ImageFilter.GaussianBlur(12))
    return np.asarray(img, dtype=np.float64)


def _moving_avg(m, window, axis):
    kernel = np.ones(window) / window
    return np.apply_along_axis(lambda r: np.convolve(r, kernel, mode="same"), axis, m)


def architectural_mask(edge_map):
    """Boolean mask of structural (long straight line) edges in the BEFORE map."""
    e = edge_map / (edge_map.max() or 1.0)
    binary = (e > 0.25).astype(np.float64)
    h = _moving_avg(binary, LINE_LEN, axis=1) > LINE_THR
    v = _moving_avg(binary, LINE_LEN, axis=0) > LINE_THR
    mask = h | v
    # Dilate by ~3px so a slightly shifted shell line still counts as surviving.
    d = mask.copy()
    for dy in (-3, -2, -1, 0, 1, 2, 3):
        for dx in (-3, -2, -1, 0, 1, 2, 3):
            d |= np.roll(np.roll(mask, dy, axis=0), dx, axis=1)
    # Rolled edges wrap; clear a border to avoid wrap artefacts.
    d[:4, :] = d[-4:, :] = False
    d[:, :4] = d[:, -4:] = False
    return d


def score(before_path, after_path):
    eb, ea = _edges(_gray(before_path)), _edges(_gray(after_path))
    mask = architectural_mask(eb)
    mb, ma = eb[mask], ea[mask]
    # Survival: for strong before-arch edges, is there after-edge energy nearby?
    strong = mb > (eb.max() * 0.25 if eb.max() else 0)
    survival = float((ma[strong] > (ea.max() * 0.15 if ea.max() else 0)).mean()) if strong.any() else 0.0
    return {
        "edge_corr": round(_corr(eb, ea), 3),
        "layout_corr": round(_corr(_blur(before_path), _blur(after_path)), 3),
        "arch_edge_corr": round(_corr(mb, ma), 3),
        "arch_survival": round(survival, 3),
        "arch_mask_coverage_pct": round(float(mask.mean()) * 100, 1),
    }


def verdict(scores):
    """Stage 2 gate — architecture-first, furniture-agnostic."""
    a, l = scores["arch_edge_corr"], scores["layout_corr"]
    if a >= 0.45 and l >= 0.80:
        return "PASS — shell preserved (architectural-mask gate)"
    if a >= 0.30 and l >= 0.70:
        return "PARTIAL — shell mostly held, review visually"
    return "FAIL — shell drifted; regeneration required"
