"""Runtime responsive helpers for NexaOffice.

The desktop UI must fit different Windows laptops, DPI scaling levels, and
split-screen window sizes.  This module keeps the sizing decisions small,
testable, and safe to call from the Tk main thread.
"""
from __future__ import annotations

from datetime import datetime
from typing import Any, Dict, List


def clamp_ui_font_scale(requested: float, screen_width: int, screen_height: int, dpi_scaling: float) -> float:
    """Return a safe runtime UI font scale for the current monitor.

    The value saved in Settings is the user's preference, but the runtime value
    should not make cards overflow on small / high-DPI laptops.  This keeps text
    readable while preventing the common 125%/150% Windows-scale clipping case.
    """
    try:
        value = float(requested)
    except Exception:
        value = 1.0
    try:
        sw = int(screen_width or 0)
        sh = int(screen_height or 0)
    except Exception:
        sw, sh = 1366, 768
    try:
        dpi = float(dpi_scaling or 1.333)
    except Exception:
        dpi = 1.333

    max_scale = 1.10
    min_scale = 0.95
    if sw <= 1366 or sh <= 768:
        max_scale = min(max_scale, 1.02)
    if dpi >= 1.60 and sw <= 1440:
        max_scale = min(max_scale, 1.00)
    if sw < 1120 or sh < 680:
        max_scale = min(max_scale, 0.98)
    return round(max(min_scale, min(max_scale, value)), 2)


def runtime_layout_bucket(width: int, height: int, dpi_scaling: float = 1.333) -> Dict[str, Any]:
    """Classify a real window into a responsive layout bucket."""
    width = int(width or 0)
    height = int(height or 0)
    try:
        dpi = float(dpi_scaling or 1.333)
    except Exception:
        dpi = 1.333
    # v2026.06.20.04: avoid over-compacting normal laptop windows.
    # A 1366x768 display often leaves less than 760px usable height after the
    # Windows taskbar and app chrome; treating that alone as compact made pages
    # look unnecessarily small.  Width is the main clipping risk; height should
    # usually be handled by scrolling.
    tiny = width < 920 or height < 560
    narrow = width < 1120
    compact = width < 1320 or height < 680
    high_dpi_fit_risk = dpi >= 1.60 and (width < 1360 or height < 760)
    if tiny:
        name = "tiny"
    elif narrow:
        name = "narrow"
    elif compact or high_dpi_fit_risk:
        name = "compact"
    else:
        name = "balanced"
    return {
        "name": name,
        "tiny": tiny,
        "narrow": narrow,
        "compact": compact or high_dpi_fit_risk,
        "high_dpi_fit_risk": high_dpi_fit_risk,
        "width": width,
        "height": height,
        "dpi_scaling": round(dpi, 3),
    }


def collect_widget_fit_issues(widget, max_items: int = 12, tolerance: int = 18) -> List[Dict[str, Any]]:
    """Collect mapped widgets whose requested size is clearly larger than actual.

    This is intentionally conservative and diagnostic-oriented.  It ignores tiny
    widgets and only reports visible, mapped widgets.  The app can use the report
    to enable compact mode and to export diagnostics when a user's PC clips UI.
    """
    issues: List[Dict[str, Any]] = []
    seen = set()

    def visit(node, depth=0):
        if len(issues) >= max_items or depth > 7:
            return
        try:
            if not node.winfo_ismapped():
                return
            cls = node.winfo_class()
            actual_w = int(node.winfo_width())
            actual_h = int(node.winfo_height())
            req_w = int(node.winfo_reqwidth())
            req_h = int(node.winfo_reqheight())
            if actual_w >= 24 and actual_h >= 16:
                overflow_w = req_w - actual_w
                overflow_h = req_h - actual_h
                # Labels often have large natural text request sizes but wrap fine;
                # report frames/buttons/entries/canvases first because they usually
                # explain real clipping.
                if cls not in {"Label", "TLabel"} and (overflow_w > tolerance or overflow_h > tolerance * 2):
                    key = str(node)
                    if key not in seen:
                        seen.add(key)
                        issues.append({
                            "widget": key[-80:],
                            "class": cls,
                            "actual": [actual_w, actual_h],
                            "requested": [req_w, req_h],
                            "overflow": [max(0, overflow_w), max(0, overflow_h)],
                        })
        except Exception:
            return
        try:
            for child in node.winfo_children():
                visit(child, depth + 1)
        except Exception:
            pass

    visit(widget)
    return issues[:max_items]


def make_layout_runtime_report(app_version: str, width: int, height: int, profile: Dict[str, Any], issues: List[Dict[str, Any]]) -> Dict[str, Any]:
    return {
        "time": datetime.now().isoformat(timespec="seconds"),
        "version": app_version,
        "window": {"width": int(width or 0), "height": int(height or 0)},
        "profile": dict(profile or {}),
        "status": "needs_compact" if issues else "ok",
        "issue_count": len(issues or []),
        "issues": list(issues or []),
    }
