from __future__ import annotations

from functools import lru_cache
import json





@lru_cache(maxsize=1)
def detail_measurements():
    """Small static provenance table; no runtime capture or old archive access."""
    from .paths import resource_dir
    return json.loads((resource_dir() / "gather-detail-calibration.json").read_text(encoding="utf-8"))


def prepared_detail_frame(context, node_id):
    """Preparation-only measured correction, NOT a recovered native caller.

    The78 individually measured PC70 frames retain their UI-unit content
    heights at the same576-UI prefab/text width. Eleven independent wide
    details agree within the old detector's three-pixel sampling interval.
    The89-UI vertical offset and14-UI inset are explicit measured calibration.
    """
    from .execution_rules import number
    from .ui_layout import LayoutError, Rect
    table = detail_measurements()
    if node_id not in table["items"]:
        raise LayoutError("unmeasured gather detail: " + str(node_id))
    measured = table["items"][node_id]
    scale = context.scale
    width = number("life.gather.calibration.frame_width_ui", 548.3026527612515) * scale
    height_ui = number("life.gather.calibration.height_ui." + node_id, measured["frame_height_ui"])
    height = height_ui * scale
    center_x = context.safe_rect.center[0]
    center_y = context.safe_rect.center[1] - number("life.gather.calibration.center_y_offset_ui", 89) * scale
    frame = Rect(center_x-width/2, center_y-height/2, width, height)
    return frame, dict(calibration_kind=table["kind"], calibration_source_index=table["source_index"],
        calibration_source_file=measured["source_file"], calibration_source_sha256=measured["source_sha256"],
        measured_frame_height_ui=measured["frame_height_ui"], applied_frame_height_ui=height_ui,
        measured_frame_rect=frame.bounds)


def prepared_detail_parent(context, node_id):
    from .execution_rules import number
    from .ui_layout import Rect
    frame, metadata = prepared_detail_frame(context, node_id)
    inset = number("life.gather.calibration.frame_inset_ui", 14) * context.scale
    width = 576 * context.scale  # recovered prefab root/text width, unchanged
    root = Rect(frame.center[0]-width/2, frame.top-inset, width, frame.height+2*inset)
    return root, metadata


def prepared_requirement_exclusions(context, card, *, scroll_y=0):
    """Preparation-only ThumbnailGroup mask, relative to the SAME card parent.

    ExplainArea is464 UI wide and right-aligns a128-square thumbnail. Tags
    have dynamic text width/placement; do not invent a fixed tag rectangle.
    Retain the existing condition envelope but exclude the source icon child.
    """
    from .execution_rules import geometry
    from .ui_layout import Rect
    x, y, width, height = geometry("gather.requirement_thumbnail_ui")
    scale = context.scale
    thumbnail = Rect(card.center[0]+x*scale, card.top+y*scale-scroll_y,
                     width*scale, height*scale)
    return (context.viewport.rect_to_normalized(thumbnail),)


def prepared_requirement_locked(image, normalized_roi, *, fraction=.015, exclusions=()):
    """Read prepared condition pixels, excluding prepared decorative children.

    No OCR, panel search or coordinate calculation is performed during input.
    """
    import numpy as np
    from .execution_rules import number
    bounds = tuple(round(value*image.size[index % 2]) for index, value in enumerate(normalized_roi))
    pixels = np.asarray(image.crop(bounds).convert("RGB")).astype("int16")
    if not pixels.size:
        return True
    eligible = np.ones(pixels.shape[:2], dtype=bool)
    for region in exclusions:
        left, top, right, bottom = (round(value*image.size[index % 2])
                                    for index, value in enumerate(region))
        left, top = max(bounds[0], left), max(bounds[1], top)
        right, bottom = min(bounds[2], right), min(bounds[3], bottom)
        if left < right and top < bottom:
            eligible[top-bounds[1]:bottom-bounds[1], left-bounds[0]:right-bounds[0]] = False
    red, green, blue = pixels[:,:,0], pixels[:,:,1], pixels[:,:,2]
    locked = ((red > number("life.requirement.red_min", 95)) &
        (green < red*number("life.requirement.green_ratio", .58)) &
        (blue < red*number("life.requirement.blue_ratio", .65)) & eligible)
    count = int(eligible.sum())
    return bool(count and locked.sum()/count > fraction)
