import functools
import json
from queue import Queue
from typing import Callable

from loguru import logger

from .CentralDispatch import SerialDispatchQueue
from .ContextUtils import get_fixed_size
from .exception_utils import try_make_lines, try_print_line
from pprint import pprint

def solve_layout(display_state: dict, screen_height: int) -> dict:
    allocated = {}
    remaining = int(screen_height)
    if remaining < 0:
        remaining = 0

    fixed_total = 0
    flex_items = []

    for key, context in display_state.items():
        layout = context.get("layout", {})
        if "height" in layout:
            h = int(layout["height"])
            h = max(0, h)
            allocated[key] = h
            fixed_total += h
        else:
            flex_value = float(layout.get("flex", 0) or 0)
            if flex_value > 0:
                min_h = int(layout.get("min_height", 0) or 0)
                max_h = layout.get("max_height", None)
                if max_h is None:
                    max_h = max(0, screen_height)  # large cap so flex distribution is not artificially clamped
                else:
                    max_h = int(max_h)
                    if max_h < 0:
                        max_h = 0
                curr = max(0, min_h)
                cap = max(0, max_h - curr)
                flex_items.append({
                    "key": key,
                    "flex": float(flex_value),
                    "curr": int(curr),
                    "cap": int(cap),
                })

    remaining = max(0, remaining - fixed_total)

    if not flex_items:
        return allocated

    sum_min = sum(it["curr"] for it in flex_items)

    if remaining <= sum_min:
        for it in flex_items:
            allocated[it["key"]] = int(it["curr"])
        return allocated

    remaining -= sum_min

    active = [it for it in flex_items if it["cap"] > 0 and it["flex"] > 0.0]

    while remaining > 0 and active:
        total_flex = sum(it["flex"] for it in active)
        if total_flex <= 0:
            break

        adds = []
        frac_order = []
        gained = 0

        for it in active:
            share = (it["flex"] / total_flex) * remaining
            base = int(share)
            add = min(it["cap"], base)
            adds.append(add)
            frac_order.append(share - base)

        for it, add in zip(active, adds):
            if add > 0:
                it["curr"] += add
                it["cap"] -= add
                remaining -= add
                gained += add

        if remaining > 0:
            if gained == 0:
                order = sorted(range(len(active)), key=lambda i: frac_order[i], reverse=True)
                for idx in order:
                    if remaining <= 0:
                        break
                    it = active[idx]
                    if it["cap"] > 0:
                        it["curr"] += 1
                        it["cap"] -= 1
                        remaining -= 1
                        gained += 1
            if remaining > 0 and gained == 0:
                for it in active:
                    if remaining <= 0:
                        break
                    if it["cap"] > 0:
                        take = min(it["cap"], remaining)
                        it["curr"] += take
                        it["cap"] -= take
                        remaining -= take
                        gained += take

        active = [it for it in active if it["cap"] > 0 and it["flex"] > 0.0]

    for it in flex_items:
        allocated[it["key"]] = int(max(0, it["curr"]))

    return allocated


class Activity:
    def __init__(self):
        self.application = None
        self.event_queue: Queue = None
        self.screen = None
        self.main_thread: SerialDispatchQueue = None
        self.display_state = {}
        self.previous_display_state = {}
        self.lifecycle_state = "init"
        self.tab_order: list = []
        self.focus: str = None
        self._region_map = {}
        self._layout_cache_key = None
        self._cached_heights = None

    def _set_focus(self, target):
        """Move focus to *target*, updating ``focused`` flags in display_state."""
        if self.focus and self.focus in self.display_state:
            self.display_state[self.focus]["focused"] = False
        self.focus = target
        if target and target in self.display_state:
            self.display_state[target]["focused"] = True

    def cycle_focus(self):
        """Advance focus to the next entry in ``tab_order``, wrapping around."""
        if not self.tab_order:
            return
        if self.focus in self.tab_order:
            idx = (self.tab_order.index(self.focus) + 1) % len(self.tab_order)
        else:
            idx = 0
        self._set_focus(self.tab_order[idx])

    def delegate_to_focused(self, event):
        """Route *event* to the ``input_handler`` of the focused context.

        Returns True if a handler was found and called, False otherwise.
        """
        if not self.focus or self.focus not in self.display_state:
            return False
        ctx = self.display_state[self.focus]
        handler = ctx.get("input_handler")
        if handler:
            handler(self.focus, ctx, event, self.event_queue)
            return True
        return False

    def _start(self, application):
        self.lifecycle_state = "starting"
        self.application = application
        self.event_queue = application.event_queue
        self.screen = application.curses_screen
        self.main_thread = application.main_thread
        self.background_thread = application.background_thread
        self.on_start()
        self.lifecycle_state = "started"
        self.refresh_screen()

    def on_start(self): pass

    def _stop(self):
        self.on_stop()
        self.lifecycle_state = "stopped"
        self.application = None

    def on_stop(self): pass

    def _layout_fingerprint(self, num_rows, num_cols):
        """Build a hashable key capturing all inputs that affect solved heights."""
        from .layout import Constraint
        parts = [num_rows, num_cols]
        for key, context in self.display_state.items():
            parts.append(key)
            layout = context.get("layout")
            if isinstance(layout, Constraint):
                parts.append(layout)
            elif isinstance(layout, dict):
                parts.append(tuple(sorted(layout.items())))
                # For auto-measured regions (not fixed, not flex), add content hints
                flex_value = float(layout.get("flex", 0) or 0)
                is_fixed = "height" in layout
                is_flex = flex_value > 0
                if not is_fixed and not is_flex:
                    if "items" in context:
                        parts.append(("items_len", len(context["items"])))
                    elif "lines" in context:
                        parts.append(("lines_len", len(context["lines"])))
                    elif "text" in context:
                        parts.append(("text_len", len(context["text"])))
                    else:
                        # Unknown auto-measured content; force re-measure
                        parts.append(("unmeasurable", id(context)))
        return tuple(parts)

    def generate_line_printers(self) -> list[Callable]:
        from .layout import Constraint, solve_constraints

        num_rows, num_cols = self.application.curses_screen.getmaxyx()

        for key, context in self.display_state.items():
            if "layout" not in context:
                raise Exception(f"Legacy display_state item detected without 'layout': {key}")

        constraint_keys = {k for k, ctx in self.display_state.items() if isinstance(ctx.get("layout"), Constraint)}
        dict_keys = {k for k, ctx in self.display_state.items() if isinstance(ctx.get("layout"), dict)}

        if constraint_keys and dict_keys:
            raise TypeError(
                f"Cannot mix Constraint layouts with dict layouts in the same display_state. "
                f"Constraint keys: {sorted(constraint_keys)}, dict keys: {sorted(dict_keys)}"
            )

        fingerprint = self._layout_fingerprint(num_rows, num_cols)
        if fingerprint == self._layout_cache_key and self._cached_heights is not None:
            heights = self._cached_heights
        elif constraint_keys:
            heights = solve_constraints(self.display_state, num_rows)
            self._layout_cache_key = fingerprint
            self._cached_heights = heights
        else:
            measured_state = {}
            for key, context in self.display_state.items():
                layout = dict(context.get("layout", {}))
                flex_value = float(layout.get("flex", 0) or 0)
                is_fixed = "height" in layout
                is_flex = (flex_value > 0)
                if not is_fixed and not is_flex:
                    min_h = int(layout.get("min_height", 0) or 0)
                    max_h_raw = layout.get("max_height", None)
                    try:
                        printers = try_make_lines(context, 1_000_000)
                        natural = len(list(printers))
                    except Exception:
                        natural = min_h
                    if max_h_raw is None:
                        natural_h = max(min_h, natural)
                    else:
                        max_h = int(max_h_raw)
                        if max_h < 0:
                            max_h = 0
                        natural_h = max(min_h, min(max_h, natural))
                    ml = layout.copy()
                    ml["height"] = int(max(0, natural_h))
                    ml.pop("flex", None)
                    measured_state[key] = {"layout": ml}
                else:
                    measured_state[key] = {"layout": layout}

            heights = solve_layout(measured_state, num_rows)
            self._layout_cache_key = fingerprint
            self._cached_heights = heights

        next_y_index = 0
        screen_line_printers = []
        region_map = {}
        for key, context in self.display_state.items():
            allocated = int(heights.get(key, 0))
            if allocated > 0:
                y_start = next_y_index
                line_printers = try_make_lines(context, allocated)
                line_printers = list(line_printers)[:allocated]
                screen_line_printers += line_printers
                next_y_index += len(line_printers)
                region_map[key] = (y_start, next_y_index)
            if next_y_index >= num_rows:
                break

        self._region_map = region_map
        return screen_line_printers

    def refresh_screen(self):
        if self.lifecycle_state != "stopped":
            screen = self.application.curses_screen
            screen_line_printers = self.generate_line_printers()

            screen.clear()
            for y, line_printer in enumerate(screen_line_printers):
                try_print_line(line_printer, screen, y)

            screen.refresh()

        self.previous_display_state = self.display_state

    def require_service(self, name, timeout=None):
        """Convenience: auto-start and block until *name* is RUNNING."""
        return self.application.require_service(name, timeout=timeout)

    def region_at_y(self, y):
        """Return the display_state key whose region contains row *y*, or None."""
        for key, (y_start, y_end) in self._region_map.items():
            if y_start <= y < y_end:
                return key
        return None

    def delegate_mouse_to_region(self, event):
        """Route a mouse event to the mouse_handler of the region at event.y.

        Returns True if a handler was found and called, False otherwise.
        """
        key = self.region_at_y(event.y)
        if key is None:
            return False
        ctx = self.display_state.get(key)
        if ctx is None:
            return False
        handler = ctx.get("mouse_handler")
        if handler is None:
            return False
        y_start = self._region_map[key][0]
        local_y = event.y - y_start
        handler(key, ctx, event, local_y, self.event_queue)
        return True

    def async_refresh_screen(self):
        self.main_thread.submit_async(self.refresh_screen)
