"""Current-frame HUD and source-derived canvas binding, without capture/input.

Only the existing menu glyph + minimap M pair is a HUD witness. Static layout,
saved preferences and current pixels remain separate evidence. Dependencies are
loaded lazily so pure source geometry works without image libraries installed.
"""
from __future__ import annotations

from dataclasses import asdict, is_dataclass
from datetime import datetime, timezone
from functools import lru_cache
import hashlib
import importlib.util
import json
import sys

from .paths import root
from .vendor.ui_layout import CanvasProfile, Rect, RectTransform, Viewport, child_rect, resolve_canvas


def _utc():
    return datetime.now(timezone.utc).isoformat()


@lru_cache(maxsize=1)
def _rules():
    return json.loads((root() / "data/arbeit-context/rules.json").read_text(encoding="utf-8"))


def read_saved_settings(settings_root=None, *, force=True):
    """Read only BaseMMPlayerPrefs/ChangedMMPlayerPrefs using the exact old reader."""
    name = "masukja2_saved_ui_settings_snapshot"
    module = sys.modules.get(name)
    if module is None:
        path = root() / "data/arbeit-context/saved_settings.py"
        spec = importlib.util.spec_from_file_location(name, path)
        module = importlib.util.module_from_spec(spec)
        sys.modules[name] = module
        try:
            spec.loader.exec_module(module)
        except Exception:
            sys.modules.pop(name, None)
            raise
    return asdict(module.GameUiSettingsReader(settings_root).read(force=force))


def _window(window):
    row = asdict(window) if is_dataclass(window) else dict(window)
    rect = row.get("client_rect") or row.get("physical_client_rect")
    if rect is None or len(rect) != 4 or any(type(v) is not int for v in rect):
        raise ValueError("physical_client_rect_required")
    left, top, right, bottom = rect
    if right <= left or bottom <= top:
        raise ValueError("physical_client_rect_invalid")
    identity = {k: row.get(k) for k in ("hwnd", "pid", "process_started_at", "exe_path")}
    return row, Viewport(right-left, bottom-top, (left, top)), identity


def prepare_hud_geometry(width, height, percent, orientation=None):
    """Original native parents/anchors; output is an observation envelope only."""
    viewport = Viewport(width, height)
    orientation = orientation or ("landscape" if width > height else "portrait")
    rules = _rules()
    row = next(p for p in rules["canvas_profiles"] if p["orientation"] == orientation)
    profile = CanvasProfile(row["reference_width"], row["reference_height"], row["minimum_width"],
                            row["provenance"], False)
    context = resolve_canvas(viewport, "pc", percent, orientation=orientation, profile=profile)
    padding = rules["screen_padding"][orientation]
    # Physical notification bar minus UI counter padding. Windows DPI is absent.
    safe_top = max(0., padding["notification_bar_pixels"] - padding["counter_padding_ui"][2]*context.scale)
    safe = Rect(0, safe_top, width, height-safe_top)
    magnets = rules["hud_geometry"]["coordinates.observation.hud.magnets"][orientation]
    parents = {}
    for name in ("menu", "map"):
        x, y, w, h = magnets[name]
        parents[name] = child_rect(safe, RectTransform((1,1), (1,1), (1,1), (x,y), (w,h)), context.scale)
    button = child_rect(parents["menu"], RectTransform((.5,.5), (.5,.5), (.5,.5), (0,0),
        tuple(rules["hud_geometry"]["coordinates.observation.hud.menu_button_size_ui"])), context.scale)
    # The M template includes its outline outside the text band. The serialized
    # minimap parent is the containing witness; no dynamic text width is guessed.
    regions = {"hud_menu": button, "hud_map_key": parents["map"]}
    from .coordinate_geometry import CLIENT_LTRB
    return {"canvas_scale": context.scale, "safe_rect": list(safe.bounds), "safe_rect_format": CLIENT_LTRB,
        "profile": {"id": row["id"], "orientation": orientation, "percent": percent,
            "reference_width": profile.reference_width, "reference_height": profile.reference_height,
            "minimum_width": profile.minimum_width, "source": row["provenance"]},
        "regions": {key: list(rect.intersection(viewport.rect).bounds) for key,rect in regions.items()
                    if rect.intersection(viewport.rect) is not None},
        "source": "MainHudModularUI/SafeArea -> orientation Magnet -> MainMenuButton98/Minimap172",
        "actuator": False}


@lru_cache(maxsize=1)
def _templates():
    import numpy as np
    from PIL import Image
    result = []
    for spec in _rules()["templates"]:
        path = root() / "data/arbeit-context" / spec["file"]
        if hashlib.sha256(path.read_bytes()).hexdigest() != spec["sha256"]:
            raise ValueError("hud_template_hash_mismatch:" + spec["id"])
        with Image.open(path) as image:
            result.append((spec, np.asarray(image.convert("RGB"))))
    return result


def _match_glyph(pixels, spec, original, bounds, canvas_scale):
    """Adapt existing match_hud_glyph; retain threshold, ink and contrast tests."""
    import cv2
    import numpy as np
    height, width = pixels.shape[:2]
    x0,y0,x1,y1 = (round(v) for v in bounds)
    x0,y0,x1,y1 = max(0,x0),max(0,y0),min(width,x1),min(height,y1)
    cut = pixels[y0:y1,x0:x1]
    if not cut.size:
        return None
    area = cv2.cvtColor(cut, cv2.COLOR_RGB2GRAY)
    best = None
    reference_scale = _rules()["template_canvas_scale"]
    for factor in (1., .97, 1.03):
        w,h = (max(3, round(size*canvas_scale/reference_scale*factor)) for size in (original.shape[1], original.shape[0]))
        if h > area.shape[0] or w > area.shape[1]:
            continue
        needle = cv2.resize(original, (w,h), interpolation=cv2.INTER_AREA)
        lo,hi = needle.min(axis=2),needle.max(axis=2).astype(np.int16)
        ink = ((lo >= 210) & (hi-lo < 60)).astype(np.uint8)
        if spec["id"] == "hud_menu":
            ink[:round(h*.5),round(w*.67):] = 0
        radius = max(1, round(canvas_scale/reference_scale*2))
        support = cv2.dilate(ink, np.ones((radius*2+1,radius*2+1),np.uint8))
        if spec["id"] == "hud_menu":
            support[:round(h*.5),round(w*.67):] = 0
        outline = (support > 0) & (ink == 0)
        if int(ink.sum()) < 5 or not outline.any():
            continue
        reference = cv2.cvtColor(needle, cv2.COLOR_RGB2GRAY)
        scores = cv2.matchTemplate(area,reference,cv2.TM_CCOEFF_NORMED,mask=support)
        scores[~np.isfinite(scores)] = -1
        _,score,_,(x,y) = cv2.minMaxLoc(scores)
        if score < spec["threshold"]:
            continue
        patch = cut[y:y+h,x:x+w]
        if np.percentile(patch.min(axis=2)[ink > 0],75) < 205:
            continue
        gray = area[y:y+h,x:x+w]
        if float(gray[ink > 0].mean()) - float(gray[outline].mean()) < 12:
            continue
        if best is None or score > best["score"]:
            best = {"score": round(min(1.,float(score)),4), "box": [x+x0,y+y0,x+x0+w,y+y0+h],
                    "basis": "upper_right_glyph_and_immediate_outline", "scale_factor": factor}
    return best


def read_context(image, window_snapshot, saved_settings=None):
    """Bind this supplied PIL frame to physical geometry, never acquire a frame.

    ``ok`` means both current glyphs uniquely fit a source parent/scale candidate.
    It does not prove idle/combat state, current quest, target visibility, input
    permission or a game-process UI tree. No OCR/CLI check participates.
    """
    result = {"ok": False, "field_hud": False, "source": "supplied_frame_hud_pair+client_native_parent_rules",
        "observed_at": _utc(), "missing": [], "profile": None, "canvas_scale": None,
        "current_instance_memory_read": False, "game_input_sent": False,
        "scope": "screen_space_default_UI_layer_visual_fit", "candidates": []}
    try:
        row,viewport,identity = _window(window_snapshot)
        result.update(viewport={"width": viewport.width,"height": viewport.height,"origin": list(viewport.origin)},
                      window_identity=identity, client_rect=list(row.get("client_rect") or row["physical_client_rect"]))
        result["window_observed_at"] = row.get("observed_at")
        result["frame_observed_at"] = getattr(image,"info",{}).get("captured_at")
        result["frame_freshness_owner"] = "caller capture receipt; this pure reader does not capture or assert freshness"
        if tuple(image.size) != (viewport.width,viewport.height):
            raise ValueError("capture_and_physical_client_size_mismatch")
        if row.get("access_blocker"):
            raise ValueError("window_access_blocked")
        import numpy as np
        pixels = np.asarray(image.convert("RGB"))
        matches = []
        for percent in (70,80,90,100):
            geometry = prepare_hud_geometry(viewport.width,viewport.height,percent)
            hits = {spec["id"]: _match_glyph(pixels,spec,template,geometry["regions"][spec["id"]],geometry["canvas_scale"])
                    for spec,template in _templates() if spec["id"] in geometry["regions"]}
            both = len(hits) == 2 and all(hits.values())
            result["candidates"].append({"percent":percent,"matched":both,"hits":hits})
            if both:
                matches.append((geometry,hits))
        if len(matches) != 1:
            result["missing"].append("current_HUD_menu_and_M_source_scale_match" if not matches else "unique_current_canvas_scale")
            return result
        geometry,hits = matches[0]
        result.update(ok=True,field_hud=True,profile=geometry["profile"],canvas_scale=geometry["canvas_scale"],
                      safe_rect=geometry["safe_rect"],hud_regions=geometry["regions"],hits=hits)
        result["profile"].update(visual_confirmed=True, style="effective_percent_only",
            binding="two_current_glyphs_inside_source_parents_at_unique_scale")
        # PC/Auto/Mobile can produce identical geometry. Do not infer that enum
        # from pixels. Saved values are separately labelled, and mismatch is not
        # silently replaced by a preferred profile.
        settings = asdict(saved_settings) if is_dataclass(saved_settings) else dict(saved_settings or {})
        if settings:
            result["saved_settings"] = settings
            if settings.get("available"):
                expected = settings.get("percent")
                if settings.get("style") == "auto":
                    short = min(viewport.width,viewport.height)
                    expected = 100 if short < 800 else 90 if short < 1100 else 80 if short < 1300 else 70
                result["saved_settings_match_current_pixels"] = expected == geometry["profile"]["percent"]
        result["uncertainty"] = ["Visual fit binds geometry, not a process-memory canvas instance or current button visibility.",
            "Historical glyph samples/native rules need update review when client UI assets change."]
        return result
    except (ValueError,TypeError,KeyError,AttributeError,ImportError) as exc:
        result["missing"].append(str(exc))
        return result


class CurrentContextReader:
    """Small callable boundary; no hidden current-frame or cross-window cache."""
    def __init__(self, settings_reader=None):
        self.settings_reader = settings_reader

    def identify(self, image, window_snapshot, saved_settings=None):
        if saved_settings is None and self.settings_reader is not None:
            saved_settings = self.settings_reader()
        return read_context(image,window_snapshot,saved_settings)

    __call__ = identify
