"""Offline source-formula candidates. No game reads, input or runtime binding.

All input/output Rects are physical client-content coordinates (no desktop origin).
Only authored layout-unit constants are multiplied by context.scale, exactly once.
Defaults which are not recovered VM semantics stay explicit in result metadata.
"""
from __future__ import annotations

from dataclasses import dataclass
import json
import math
from pathlib import Path
from types import MappingProxyType
from typing import Mapping, Sequence

from .paths import resource_dir
from .ui_layout import CanvasContext, LayoutError, Rect


_CONSTANT_NAMES = frozenset({
    "group_width", "button_height", "button_gap", "settled_bottom",
    "content_padding_x", "party_landscape_right", "party_portrait_area_height",
    "party_landscape_area_height", "portrait_auto_equip_area_height",
    "ordinary_area_height", "auto_retry_height", "auto_retry_gap", "common_area_height",
})


@dataclass(frozen=True)
class ResultLayoutRules:
    revision: str
    compiled_source_sha256: str
    constants: Mapping[str, float]


@dataclass(frozen=True)
class ResultLayout:
    rects: Mapping[str, Rect]
    group: Rect | None
    content_parent: Rect
    button_area_height: float
    metadata: Mapping[str, object]


def load_result_layout_rules(path: str | Path | None = None) -> ResultLayoutRules:
    """Load data, never expressions or executable code; invalid data is not fallback."""
    path = Path(path) if path is not None else resource_dir() / "ui-rules/source-result-layouts.json"
    try:
        raw = path.read_bytes()
        if len(raw) > 65536:
            raise ValueError("result rules exceed bounded data size")
        def unique(pairs):
            result = {}
            for key, value in pairs:
                if key in result:
                    raise ValueError("duplicate rule key")
                result[key] = value
            return result
        data = json.loads(raw.decode("utf-8-sig"), object_pairs_hook=unique)
        constants = data["constants"]
        if type(data["schema_version"]) is not int or data["schema_version"] != 1:
            raise ValueError("unsupported result rule schema")
        if not isinstance(constants, dict) or set(constants) != _CONSTANT_NAMES:
            raise ValueError("result constant set changed")
        if any(type(x) not in (float, int) or not math.isfinite(x) or x <= 0
               for x in constants.values()):
            raise ValueError("result constants must be positive finite numbers")
        sha = data["compiled_source_sha256"]
        if not isinstance(sha, str) or len(sha) != 64 or any(c not in "0123456789abcdef" for c in sha):
            raise ValueError("missing compiled source fingerprint")
        if not isinstance(data["revision"], str) or not data["revision"].strip():
            raise ValueError("missing rule revision")
        return ResultLayoutRules(data["revision"], sha, MappingProxyType(dict(constants)))
    except (OSError, ValueError, KeyError, TypeError) as exc:
        raise LayoutError(f"invalid source result layout data: {exc}") from exc


def _parent(context, rules, safe_area, content_parent):
    if not isinstance(context, CanvasContext):
        raise LayoutError("a resolved CanvasContext is required")
    if safe_area is not None and content_parent is not None:
        raise LayoutError("supply safe_area or already-padded content_parent, not both")
    area = context.safe_rect if safe_area is None else safe_area
    parent = content_parent
    if parent is None:
        if not isinstance(area, Rect) or not context.root_rect.contains(area):
            raise LayoutError("safe area must be a physical client Rect")
        padding = rules.constants["content_padding_x"] * context.scale
        parent = Rect(area.left + padding, area.top, area.width - 2 * padding, area.height)
    if not isinstance(parent, Rect) or not context.root_rect.contains(parent):
        raise LayoutError("content parent must be a physical client Rect")
    return parent


def _roles(roles):
    if isinstance(roles, (str, bytes)):
        raise LayoutError("roles must be an ordered sequence")
    values = tuple(roles)
    if not values or len(values) > 32 or len(set(values)) != len(values):
        raise LayoutError("result roles must be nonempty, bounded and unique")
    if any(not isinstance(v, str) or not v.strip() for v in values):
        raise LayoutError("every result button needs an explicit semantic role")
    return values


def dungeon_result_buttons(
    context: CanvasContext, ordered_roles: Sequence[str], *, party=False,
    is_landscape=None, safe_area: Rect | None = None, content_parent: Rect | None = None,
    show_buttons=True, auto_equip=False, auto_retry_toggle=False,
    rules: ResultLayoutRules | None = None,
) -> ResultLayout:
    """MMML1395 ->1399/205 ->1391; caller supplies the actual ordered button list.

    party means partyInfoList.count>0, not the selected matchmaking preference.
    No caption/role order is guessed. Hidden buttons return an empty target map.
    Unspecified horizontal placement uses the declared center *trial assumption*.
    """
    rules = load_result_layout_rules() if rules is None else rules
    roles = _roles(ordered_roles)
    if any(type(v) is not bool for v in (party, show_buttons, auto_equip, auto_retry_toggle)):
        raise LayoutError("result state flags must be explicit booleans")
    landscape = context.orientation == "landscape" if is_landscape is None else is_landscape
    if type(landscape) is not bool or landscape != (context.orientation == "landscape"):
        raise LayoutError("orientation flag differs from the canvas context")
    parent = _parent(context, rules, safe_area, content_parent)
    c, s = rules.constants, context.scale
    area_key = ("party_landscape_area_height" if landscape else "party_portrait_area_height") if party else (
        "portrait_auto_equip_area_height" if not landscape and auto_equip else "ordinary_area_height")
    area_units = c[area_key] + (c["auto_retry_height"] + c["auto_retry_gap"] if auto_retry_toggle else 0)
    width, height, gap = c["group_width"]*s, c["button_height"]*s, c["button_gap"]*s
    left = parent.right - (c["party_landscape_right"]*s + width) if party and landscape else parent.center[0] - width/2
    group = Rect(left, parent.bottom - (c["settled_bottom"] + c["button_height"])*s, width, height)
    meta = MappingProxyType({
        "family": "dungeon-result-mmml1395", "revision": rules.revision,
        "compiled_source_sha256": rules.compiled_source_sha256,
        "source_functions": (1395, 1399, 205, 1391, 163),
        "horizontal_placement": "source-party-right56" if party and landscape else "candidate-default-center",
        "safe_area_from_context_default": safe_area is None and content_parent is None,
        "runtime_click_approved": False, "animation": "settled-only",
        "candidate_assumptions": () if party and landscape else ("unset horizontal placement is centered",),
    })
    if not show_buttons:
        return ResultLayout(MappingProxyType({}), None, parent, area_units*s, meta)
    cell_width = (width - gap * (len(roles)-1)) / len(roles)
    if cell_width <= 0 or not parent.contains(group):
        raise LayoutError("result buttons do not fit the supplied parent; no stretching fallback")
    rects = {role: Rect(left + i*(cell_width + gap), group.top, cell_width, height)
             for i, role in enumerate(roles)}
    return ResultLayout(MappingProxyType(rects), group, parent, area_units*s, meta)


def common_result_confirm(
    context: CanvasContext, role="processing.result.confirm", *, family="facility",
    safe_area: Rect | None = None, content_parent: Rect | None = None,
    rules: ResultLayoutRules | None = None,
) -> ResultLayout:
    """1414 ->1399 ->1410 ->199 single-button trial geometry.

    gridCol='1/3' is modelled as a full three-track span, explicitly a candidate.
    family='snack' additionally marks the separate craft-result prefab binding
    unresolved; it is NOT a claim that1414 (title '가공 완료') creates snack results.
    """
    rules = load_result_layout_rules() if rules is None else rules
    _roles((role,))
    if family not in ("facility", "snack"):
        raise LayoutError("unknown common-result candidate family")
    parent = _parent(context, rules, safe_area, content_parent)
    c, s = rules.constants, context.scale
    group = Rect(parent.center[0] - c["group_width"]*s/2,
                 parent.bottom - (c["settled_bottom"] + c["button_height"])*s,
                 c["group_width"]*s, c["button_height"]*s)
    if not parent.contains(group):
        raise LayoutError("confirmation does not fit supplied parent; no stretching fallback")
    assumptions = ("unset horizontal placement is centered", "gridCol 1/3 spans all three tracks")
    if family == "snack":
        assumptions += ("TitledScrollUI_Craft_Result prefab caller/parent is not yet linked",)
    meta = MappingProxyType({
        "family": family, "revision": rules.revision,
        "compiled_source_sha256": rules.compiled_source_sha256,
        "source_functions": (1414, 1399, 205, 1410, 199, 163),
        "caller_binding": "source-facility-result" if family == "facility" else "candidate-separate-craft-result-prefab",
        "safe_area_from_context_default": safe_area is None and content_parent is None,
        "candidate_assumptions": assumptions, "runtime_click_approved": False,
        "animation": "settled-only; facility delay0.6s + bottom tween0.5s",
    })
    return ResultLayout(MappingProxyType({role: group}), group, parent, c["common_area_height"]*s, meta)
