"""Owned processing storage-transfer tail, using the saved common-popup source.

MMML202/210 provide button geometry, not the caller's live uiData binding. That
binding and the body search height stay explicit trials. OCR proves a bounded
storage-material question and button state; its boxes never supply input points.
"""
from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
import re
from types import MappingProxyType

from .errors import InputInterrupted
from .focused_ocr import FocusedOCR, normalized
from .processing_recovery import ProcessingViewUncertain, view_require
from .ui_layout import CanvasContext, LayoutError, Rect


ROLE = "processing.storage_confirm"
_COMPILED_SHA = "b772d6c351958e614170ae608855f3fd1afd4d506198dd090723af820aa238fa"
_FORBIDDEN = ("구매", "판매", "강화", "합성", "뽑기", "즉시완료", "가공취소", "제작취소", "가공을취소", "제작을취소",
              "정령", "날개", "은동전", "공물", "캐시", "다이아", "거래소", "입장", "부활",
              "보관함확장", "용량증가", "초월", "돌파", "분해하시", "분해할까요")


@dataclass(frozen=True)
class StorageGeometry:
    rect: Rect
    ocr_rect: Rect | None
    visible: bool
    enabled: bool
    metadata: object
    visible_rect: Rect | None


def calculate_storage_geometry(context, *, state=None, enabled_roles=()):
    """Root-derived two-button common popup; absent state is OCR-only geometry."""
    if not isinstance(context, CanvasContext):
        raise LayoutError("resolved storage-popup canvas required")
    state = dict(state or {})
    enabled = frozenset(enabled_roles)
    if enabled - {ROLE}:
        raise LayoutError("only storage confirmation may be enabled")
    if enabled:
        if any(state.get(key) is not True for key in
               ("owned_registration", "storage_required", "popup_visible", "settled")):
            raise LayoutError("owned current storage-transfer state required")
        if any(not isinstance(state.get(key), str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,96}", state[key])
               for key in ("recipe_id", "facility_id")):
            raise LayoutError("exact processing recipe/facility identity required")
    safe, scale = context.safe_rect, context.scale
    confirm = Rect(safe.center[0]+4*scale, safe.bottom-162*scale, 274*scale, 80*scale)
    cancel = Rect(safe.center[0]-278*scale, confirm.top, 274*scale, 80*scale)
    #Frame height is content-driven. Only this OCR envelope, never an actuator,
    #uses an authored maximum600 UI units above the source footer.
    body_bottom = confirm.top-16*scale
    body_top = max(safe.top, body_bottom-600*scale)
    if body_bottom <= body_top:
        raise LayoutError("storage popup has no readable source body area")
    body = Rect(safe.center[0]-278*scale, body_top, 556*scale, body_bottom-body_top)
    assumptions = ("storage transfer caller uses CommonPopup_CancelConfirm202 -> CommonFrame_Popup210 as a trial binding",
                   "default width640; bottom-attached; Cancel+Confirm only; no custom width or third button",
                   "dynamic popup body height not recovered; authored OCR-only envelope600 UI units",
                   "popup checkbox state is not changed")
    result = {}
    for role, rect, actuator in ((ROLE, confirm, True), ("processing.storage.cancel", cancel, False),
                                 ("processing.storage.body", body, False)):
        crop = rect.intersection(safe)
        full = crop is not None and crop.contains(rect)
        metadata = MappingProxyType(dict(source_functions=(202, 210), compiled_source_sha256=_COMPILED_SHA,
            source="CommonPopup.mmml.xml:202/grid -> CommonUtility.mmml.xml:210/default frame",
            source_pc_ranges=((1574, 1702), (298, 454)), source_button_grid="1fr 8px 1fr; height80",
            coordinate_space="physical-client", geometry_kind="source-popup-candidate-binding",
            provenance="source-with-experimental-assumption", assumptions=assumptions, scale=scale,
            candidate=True, runtime_click_approved=False, actuator=actuator,
            recipe_id=state.get("recipe_id"), facility_id=state.get("facility_id"),
            allowed_cost_context="owned-storage-material-transfer-only; existing gold/equipment-parts variant",
            cost_policy_source="resources/action-paths.json processing.storage_confirm + existing storage-parts flow",
            animation="settled-only", checkboxes_modified=False,
            normalized_roi=context.viewport.rect_to_normalized(crop) if crop is not None else None,
            enabled_sample_bounds=rect.inset(min(6*scale, rect.height*.1)).pixel_bounds()))
        result[role] = StorageGeometry(rect, crop, full, role in enabled and full and actuator, metadata, crop)
    return MappingProxyType(result)


def storage_question(text):
    """Authored storage-only semantic grammar, not a generic Confirm detector."""
    value = normalized(text)
    if not value or any(term in value for term in _FORBIDDEN):
        return False
    if not re.search(r"(?:보관함.{0,100}재료|재료.{0,100}보관함)", value):
        return False
    # Actual saved client popup (20260914 jobs-bank-process-first-batch):
    # "재료를 전송하시겠습니까? 부족한 재료는 보관함에서 가져옵니다."
    # It has neither '사용' nor '가공'; do not require words absent from it.
    if ("재료를전송하시겠습니까" in value
            and "보관함에서가져옵니다" in value):
        return True
    transfer = any(word in value for word in ("가져와", "가져오", "가져올", "꺼내", "꺼낼", "인출", "전송"))
    use_stored = bool(re.search(r"보관함(?:의|에있는|에보관된)?재료(?:를|을)?사용", value))
    if not (transfer or use_stored):
        return False
    if not any(word in value for word in ("사용", "가공", "제작", "가져올까요", "꺼낼까요")):
        return False
    return any(word in value for word in ("겠습니까", "할까요", "가져올까요", "꺼낼까요", "하시겠어요", "사용합니다", "진행합니다", "가져옵니다"))


def storage_popup_visible(ocr, image, context, *, rows=None):
    """Read text and enabled backing only INSIDE source-calculated crops."""
    if tuple(image.size) != (context.viewport.width, context.viewport.height):
        return False
    rows = calculate_storage_geometry(context) if rows is None else rows
    def field(role, name):
        row = rows[role]
        return row[name] if isinstance(row, Mapping) else getattr(row, name)
    if not all(field(role, "visible") and field(role, "ocr_rect") is not None
               for role in (ROLE, "processing.storage.cancel", "processing.storage.body")):
        return False
    def text(role):
        return normalized(ocr.text(image, field(role, "metadata")["normalized_roi"]))
    if text(ROLE) not in ("확인", "space확인") or text("processing.storage.cancel") not in ("취소", "esc취소"):
        return False
    if not storage_question(text("processing.storage.body")):
        return False
    import numpy as np
    pixels = np.asarray(image.crop(field(ROLE, "metadata")["enabled_sample_bounds"]).convert("RGB")).astype("int16")
    if not pixels.size:
        return False
    red, green, blue = pixels[:, :, 0], pixels[:, :, 1], pixels[:, :, 2]
    enabled = (green > 105) & (green > red*1.22) & (green > blue*1.05)
    return bool(enabled.mean() >= .55)


def storage_recipe_visible(ocr, image, runtime, recipe):
    """Re-own only a changed/restored pending popup, using existing detail ROI.

    Modal dimming is expected here: do not require the background Register
    button to be green. Exact located labels retain '+' and material variants.
    An obscured detail is unknown, never permission to click through a popup.
    """
    from . import catalog
    from .adaptive_life import compact, unique_rows, AdaptiveLife
    band = runtime.prepared_bundle("life.observation")["processing.recipe.body"]["metadata"]["normalized_roi"]
    rows = unique_rows(ocr, image, (band,))
    headings = [row for row in rows if compact(row.text) == "필요한재료"]
    if len(headings) != 1:
        return False
    heading = headings[0]
    names = [row for row in rows if compact(row.text) == compact(AdaptiveLife._display_name(recipe))
             and row.region[3] <= heading.region[1]]
    if len(names) != 1:
        return False
    known = {compact(material["name"]) for item in catalog.processing_catalog()["recipes"]
             for material in item["materials"]}
    actual = {compact(row.text) for row in rows if row.region[1] >= heading.region[3]
              and compact(row.text) in known}
    return actual == {compact(material["name"]) for material in recipe["materials"]}


@dataclass
class StorageRegistration:
    recipe_id: str
    facility_id: str
    layout_signature: object
    identity: tuple
    resume_generation: int
    confirm_attempted: bool = False


class SourceStorageConfirmation:
    def __init__(self, service, *, processing_port=None):
        self.service = service
        self.processing_port = processing_port
        self._owner = None

    def _context(self):
        runtime = self.service._ui_route_runtime()
        snapshot = runtime.refresh(self.service.window, force=True)
        view_require(snapshot.available, "보관함 재료 확인창의 원본 계산 문맥 재확인 필요")
        return runtime, snapshot

    def arm(self, recipe, *, recovered=False):
        """Arm the sent registration, or require detail proof for old receipts."""
        self.service.check()
        _, snapshot = self._context()
        window = self.service.window
        self._recipe = recipe
        self._requires_recipe = recovered
        self._owner = StorageRegistration(recipe["id"], recipe["group"], snapshot.signature,
            (getattr(window, "hwnd", None), getattr(window, "pid", None)),
            getattr(self.service, "input_resume_generation", 0))
        return self._owner

    def confirm_if_visible(self, owner, *, before_send=None, pause=True):
        """At most once per fresh observation; absent/hidden prompts are skipped.

        A pending registration can create a new observer to retry Confirm only
        while this exact current storage popup remains visible. Register itself
        is never repeated, and the persisted sent flag is history, not a ban.
        """
        if not isinstance(owner, StorageRegistration) or owner is not self._owner:
            return False
        s = self.service
        s.check()
        runtime, snapshot = self._context()
        def owned_current():
            s.check()
            window = s.window
            port = self.processing_port
            if port is not None and (port.group != owner.facility_id or not isinstance(port.recipe, dict)
                    or port.recipe.get("id") != owner.recipe_id or port.recipe.get("group") != owner.facility_id):
                return False
            return (getattr(window, "is_foreground", lambda: True)() is True
                    and (getattr(window, "hwnd", None), getattr(window, "pid", None)) == owner.identity
                    and getattr(s, "input_resume_generation", 0) == owner.resume_generation)
        if (getattr(s.window, "hwnd", None), getattr(s.window, "pid", None)) != owner.identity:
            raise ProcessingViewUncertain("등록 후 게임 창이 변경되어 재료 확인을 보내지 않습니다.")
        reown = self._requires_recipe or not owned_current() or snapshot.signature != owner.layout_signature
        if s.ocr is None:
            s.ocr = FocusedOCR()
        identity = dict(recipe_id=owner.recipe_id, facility_id=owner.facility_id)
        rows = runtime.prepared_bundle("processing.storage_confirm", **identity)
        def visible(image, context):
            return storage_popup_visible(s.ocr, image, context, rows=rows)
        observed_generation = getattr(s, "input_resume_generation", 0)
        image = s.window.capture()
        if not visible(image, snapshot.context):
            return False
        if reown:
            self._requires_recipe = True
            view_require(storage_recipe_visible(s.ocr, image, runtime, self._recipe),
                "등록 후 문맥 변경 · 현재 보관함 전송창의 품목·재료 재확인 필요")
            owner.layout_signature = snapshot.signature
            owner.resume_generation = observed_generation
        def current():
            if not owned_current():
                return False
            _, fresh = self._context()
            runtime._same_snapshot(fresh, snapshot)
            image = s.window.capture()
            return (visible(image, fresh.context)
                    and (not reown or storage_recipe_visible(s.ocr, image, runtime, self._recipe))
                    and owned_current())
        try:
            reference = runtime.prepared_ref("processing.storage_confirm", ROLE, **identity)
            def attempting():
                owner.confirm_attempted = True
                if before_send is not None:
                    before_send()
            sent = s.processing_input("click", reference, guard=current, skip_if_guard_fails=True,
                                      before_send=attempting)
            if sent is not False:
                self._requires_recipe = False
                if pause:
                    s.processing_pause(.65)
                runtime.release_scene()
                return True
            return False
        except (LayoutError, InputInterrupted) as exc:
            runtime.release_scene()
            raise ProcessingViewUncertain("보관함 재료 확인 좌표/팝업 재확인 필요: " + str(exc)) from exc
