"""Prepare life OCR parents once, alongside the source coordinate catalogue.

These are observation-only rectangles, never click authority. Dynamic text
heights are not inferred from screen percentages: unresolved heights use a
named source-parent envelope and remain explicit trial assumptions.
"""
from __future__ import annotations

from . import catalog
from .source_life_routes import resolve_life_scene
from .source_processing_storage import calculate_storage_geometry
from .source_result_layouts import common_result_confirm
from .ui_layout import LayoutError, Rect
from .execution_rules import number


def compile_life_observation(context, processing_rules):
    """Pure preparation output for the single ``life.observation`` bundle."""
    safe, s = context.safe_rect, context.scale
    landscape = context.orientation == "landscape"
    records = {}

    def add(role, rect, *, source, assumptions=(), ocr_padding_ui=0):
        # Text detectors need context outside the authored glyph/child box.
        # Keep the source rectangle and every actuator/pixel sample unchanged.
        ocr_padding_ui = number("life.observation."+role+".padding_ui", ocr_padding_ui)
        pad = ocr_padding_ui*s
        crop = Rect(rect.left-pad, rect.top-pad, rect.width+2*pad,
                    rect.height+2*pad).intersection(safe)
        if crop is None:
            raise LayoutError("life observation parent outside game area: " + role)
        records[role] = dict(rect=rect, ocr_rect=crop, visible=True, enabled=False,
            metadata=dict(normalized_roi=context.viewport.rect_to_normalized(crop),
                source=source, scale=s, coordinate_space="physical-client",
                observation_only=True, actuator=False, runtime_click_approved=False,
                ocr_padding_ui=ocr_padding_ui,
                candidate_assumptions=tuple(assumptions)))

    def union(rects):
        x1, y1 = min(r.left for r in rects), min(r.top for r in rects)
        x2, y2 = max(r.right for r in rects), max(r.bottom for r in rects)
        return Rect(x1, y1, x2-x1, y2-y1)

    # Shared header ends at the authored 96-unit content start. The facility
    # title itself belongs to FacilityUI911.topinfo, NOT the screen header.
    add("processing.menu.heading", Rect(safe.left+32*s, safe.top, safe.width-96*s, 96*s),
        source="CraftingUI905 header / SafeArea205")
    title_left = safe.left + (96 if landscape else 32)*s
    title_top = safe.top + (140 if landscape else 178)*s
    title_width = (488*s if landscape else safe.width-64*s)
    add("processing.facility.heading", Rect(title_left, title_top-8*s, title_width, 40*s),
        source="FacilityUI911 topinfo padding20 + title24",
        assumptions=("title OCR allows eight local units above and below its declared line",),
        ocr_padding_ui=24)

    ids = tuple(group["id"] for group in catalog.processing_catalog()["groups"])
    facility = resolve_life_scene(context, "processing.facilities",
        state=dict(ordered_ids=ids, scroll_x=0, scroll_y=0), processing_rules=processing_rules)
    # Full scroll receiver, not only rows initially visible: a resumed list
    # may have retained its prior offset before the authored reset tape runs.
    x1, y1, x2, y2 = next(iter(facility.values())).metadata["parent_rect"]
    add("processing.facilities.cards", Rect(x1,y1,x2-x1,y2-y1),
        source="CraftingUI905 -> facility_card scroll parent")

    collect = []
    for count in range(1, 8):
        rows = resolve_life_scene(context, "processing.collect", state=dict(queue_count=count,
            facility_ready=True, full_safe_area=True, collect_enabled=True,
            move_visible=False, upgrade_visible=False), processing_rules=processing_rules)
        collect.append(rows["processing.collect_all"].ocr_rect)
    add("processing.collect", union(collect), source="FacilityUI911/GetAllButton912 queue branches1..7")
    travel = resolve_life_scene(context, "processing.facility.travel", state=dict(facility_id=ids[0],
        remote=True, move_visible=True, upgrade_visible=False), processing_rules=processing_rules)
    move = travel["processing.facility.travel"]
    top1, top2 = move.metadata["button_top_bounds"]
    add("processing.facility.travel", Rect(move.rect.left, top1, min(572*s,safe.right-move.rect.left), top2+48*s-top1),
        source="FacilityUI911/MoveButton908 optional upgrade+gap12+move; description34..48")

    recipe = catalog.processing_catalog()["recipes"][0]
    register = resolve_life_scene(context, "processing.register", state=dict(
        facility_id=recipe["group"], recipe_id=recipe["id"], local_detail=True,
        register_enabled=True, settled=True, scroll_y=0), processing_rules=processing_rules)["processing.register"]
    sheet_left, sheet_right = register.metadata["sheet_horizontal_bounds"]
    # The content-sized sheet has a dynamic top. Its 720-unit source width,
    # 48-unit inner padding and CTA bottom are fixed. Read this bounded parent
    # once and distinguish title/material rows semantically within that read.
    body_top = safe.top + 96*s
    add("processing.recipe.body", Rect(sheet_left+48*s, body_top,
        sheet_right-sheet_left-96*s, register.rect.top-body_top),
        source="AlteringPanelUI889 -> BottomSheet209 width720/padding48",
        assumptions=("dynamic content top is unresolved; OCR-only parent envelope begins below header96",))
    add("processing.register", register.ocr_rect, source="AlteringPanelUI889 final CTA163")
    # Slot detail is ESC-only recovery. It uses the same owned bottom-sheet
    # family envelope; it never supplies a cancel-production click target.
    add("processing.slot.detail", Rect(sheet_left, body_top, sheet_right-sheet_left,
        safe.bottom-32*s-body_top), source="FacilityUI owned bottom-sheet detail family",
        assumptions=("slot child content height is dynamic; parent-width OCR-only envelope",))

    popup = calculate_storage_geometry(context)
    add("processing.travel.question", union([item.ocr_rect for item in popup.values()]),
        source="CommonPopup202/Frame210 bottom attachment; exact travel question still required",
        assumptions=("dynamic popup body uses600-unit source-parent observation envelope",))
    add("processing.travel.confirm", popup["processing.storage_confirm"].rect,
        source="Owned facility travel CommonPopup202/210 confirm button")
    add("processing.travel.cancel", popup["processing.storage.cancel"].rect,
        source="Owned facility travel CommonPopup202/210 cancel button")
    result = common_result_confirm(context, family="facility")
    add("processing.result.confirm", result.rects["processing.result.confirm"],
        source="TitledScrollUI result common footer")
    add("processing.result.title", Rect(safe.center[0]-210*s, safe.top+160*s, 420*s, 130*s),
        source="TitledScrollUI common result heading",
        assumptions=("heading parent uses the shared authored result-title calibration",))

    node = catalog.gather_catalog()["groups"][0]["nodes"][0]
    link = resolve_life_scene(context, "gather.travel", state=dict(node_id=node["id"],
        link_kind="fishing" if node["id"].startswith("fishing-") else "nearby",
        detail_confirmed=True, requirement_met=True, link_confirmed=True, settled=True,
        scroll_y=0), processing_rules=processing_rules)["gather.travel"]
    x1,y1,x2,y2 = link.metadata["parent_rect"]
    card = Rect(x1,y1,x2-x1,y2-y1)
    assumptions = link.metadata["candidate_assumptions"]
    add("gather.detail.title", Rect(card.center[0]-232*s, card.top+24*s, 464*s, 159*s),
        source="DetailCardUILayout ExplainArea source128 + header/padding", assumptions=assumptions)
    add("gather.detail.requirements", Rect(card.center[0]-232*s, card.top+55*s, 464*s, 190*s),
        source="DetailCardUILayout Explain128 + gap30 + Mission32", assumptions=assumptions)
    from .gather_geometry import prepared_requirement_exclusions
    records["gather.detail.requirements"]["metadata"]["normalized_exclusions"] = prepared_requirement_exclusions(context, card)
    add("gather.detail.link", Rect(card.center[0]-232*s, card.bottom-86*s, 464*s, 34*s),
        source="DetailCardUILayout BottomArea34 / Button_HowToGet", assumptions=assumptions,
        ocr_padding_ui=8)
    return records
