"""
Ratatui-inspired constraint-based layout solver.

Constraint types
----------------
Length(n)       — exact n lines; allocated first, always satisfied.
Percentage(pct) — pct % of total screen height (0–100); allocated second.
Min(n)          — at least n lines; participates in proportional fill (weight=1).
Max(n)          — at most n lines; participates in proportional fill (weight=1), capped.
Fill(weight)    — proportional share of remaining space by weight; allocated last.

Activities use constraint layouts by setting each region's "layout" key to one of
these objects instead of a plain dict.  All regions in a display_state must use
the same style — mixing Constraint objects with plain dicts raises TypeError (caught
in Activity.generate_line_printers).

Usage example::

    from pyos import Length, Percentage, Fill, Min, Max
    from pyos.printers.TopBar import TopBar

    self.display_state = {
        "top":  {**TopBar.display_state(...), "layout": Length(2)},
        "info": {**MultilineText.display_state(...), "layout": Percentage(30)},
        "body": {**ScrollList.display_state(...), "layout": Fill(1)},
    }
"""

from __future__ import annotations

from dataclasses import dataclass, field


class Constraint:
    """Base class for all layout constraints."""


@dataclass(frozen=True)
class Length(Constraint):
    """Exact height.  Highest priority — always satisfied before any other type."""
    n: int


@dataclass(frozen=True)
class Percentage(Constraint):
    """Fraction of the total screen height.  Allocated after Length regions."""
    pct: float  # 0–100


@dataclass(frozen=True)
class Min(Constraint):
    """At least *n* lines.  Participates in proportional fill with weight 1."""
    n: int


@dataclass(frozen=True)
class Max(Constraint):
    """At most *n* lines.  Participates in proportional fill with weight 1, capped."""
    n: int


@dataclass(frozen=True)
class Fill(Constraint):
    """Proportional share of remaining space.  Lowest priority."""
    weight: float = 1.0


# ---------------------------------------------------------------------------
# Solver
# ---------------------------------------------------------------------------

def _distribute_remainder(items: list, floored: list[int], exact: list[float], pool: int) -> list[int]:
    """Largest-remainder (Bresenham) integer distribution.

    Given a list of floor-allocated integers and their fractional parts,
    distribute *pool* extra units one-by-one to the items with the largest
    fractional parts.  Returns the adjusted list.
    """
    result = list(floored)
    fracs = [(exact[i] - floored[i], i) for i in range(len(items))]
    fracs.sort(key=lambda x: x[0], reverse=True)
    for _, i in fracs:
        if pool <= 0:
            break
        result[i] += 1
        pool -= 1
    return result


def solve_constraints(display_state: dict, screen_height: int) -> dict[str, int]:
    """Allocate screen rows to display_state regions using typed Constraint objects.

    Parameters
    ----------
    display_state:
        The activity's display_state dict.  Each value must have a ``"layout"``
        key whose value is a :class:`Constraint` instance.
    screen_height:
        Total available rows (e.g. ``curses_screen.getmaxyx()[0]``).

    Returns
    -------
    dict mapping region name → allocated row count.
    """
    total = max(0, int(screen_height))
    allocated: dict[str, int] = {}
    remaining = total

    # Collect constraints
    constraints: dict[str, Constraint] = {
        k: ctx["layout"] for k, ctx in display_state.items()
    }

    # ------------------------------------------------------------------
    # Pass 1 — Length (exact, highest priority)
    # ------------------------------------------------------------------
    for key, c in constraints.items():
        if isinstance(c, Length):
            h = max(0, int(c.n))
            allocated[key] = h
            remaining = max(0, remaining - h)

    # ------------------------------------------------------------------
    # Pass 2 — Percentage (fraction of *total*, not remaining)
    # ------------------------------------------------------------------
    pct_keys = [k for k, c in constraints.items() if isinstance(c, Percentage)]
    if pct_keys:
        exact_shares = [(c.pct / 100.0) * total for k, c in constraints.items() if isinstance(c, Percentage)]
        exact_shares_clamped = [max(0.0, s) for s in exact_shares]
        floored = [int(s) for s in exact_shares_clamped]

        # Cap total pct allocation to remaining
        pct_sum = sum(floored)
        if pct_sum > remaining:
            # Scale down proportionally
            scale = remaining / pct_sum if pct_sum > 0 else 0.0
            floored = [int(h * scale) for h in floored]
            exact_shares_clamped = [s * scale for s in exact_shares_clamped]
            pct_sum = sum(floored)

        # Extra units = floor(sum(exact)) - sum(floor(exact_i))  — the Bresenham carry
        exact_total = sum(exact_shares_clamped)
        extra = max(0, min(remaining - pct_sum, int(exact_total) - pct_sum))
        adjusted = _distribute_remainder(pct_keys, floored, exact_shares_clamped, extra)

        for key, h in zip(pct_keys, adjusted):
            allocated[key] = h
            remaining = max(0, remaining - h)

    # ------------------------------------------------------------------
    # Pass 3 — Min / Max / Fill share remaining proportionally
    # ------------------------------------------------------------------
    flex_keys = [k for k in constraints if k not in allocated]
    if not flex_keys:
        return allocated

    # Build flex items
    items: list[dict] = []
    for key in flex_keys:
        c = constraints[key]
        if isinstance(c, Fill):
            items.append({"key": key, "min": 0, "max": None, "weight": max(0.0, float(c.weight))})
        elif isinstance(c, Min):
            items.append({"key": key, "min": max(0, int(c.n)), "max": None, "weight": 1.0})
        elif isinstance(c, Max):
            items.append({"key": key, "min": 0, "max": max(0, int(c.n)), "weight": 1.0})
        else:
            # Unknown constraint — allocate 0
            items.append({"key": key, "min": 0, "max": 0, "weight": 0.0})

    # Deduct minimums
    min_total = sum(it["min"] for it in items)
    flex_pool = max(0, remaining - min_total)

    # Compute proportional share of flex_pool
    total_weight = sum(it["weight"] for it in items)
    if total_weight > 0 and flex_pool > 0:
        exact_flex = [
            (it["weight"] / total_weight) * flex_pool
            for it in items
        ]
        floored_flex = [int(s) for s in exact_flex]
        given = sum(floored_flex)
        extra = flex_pool - given
        floored_flex = _distribute_remainder(items, floored_flex, exact_flex, extra)
    else:
        floored_flex = [0] * len(items)

    # Assign: min + flex share, then apply max cap
    pre_cap = [it["min"] + f for it, f in zip(items, floored_flex)]

    # Apply Max cap and collect excess
    excess = 0
    for i, it in enumerate(items):
        if it["max"] is not None and pre_cap[i] > it["max"]:
            excess += pre_cap[i] - it["max"]
            pre_cap[i] = it["max"]

    # Redistribute excess to uncapped Fill/Min items
    if excess > 0:
        uncapped = [i for i, it in enumerate(items) if it["max"] is None and it["weight"] > 0]
        if uncapped:
            share, leftover = divmod(excess, len(uncapped))
            for i in uncapped:
                pre_cap[i] += share
            # Give leftover one-by-one to first items
            for i in uncapped[:leftover]:
                pre_cap[i] += 1

    for it, h in zip(items, pre_cap):
        allocated[it["key"]] = max(0, h)

    return allocated
