#!/usr/bin/env python3

from __future__ import annotations

import curses
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Set


DEFAULT_ARCH = "i386"
SUPPORTED_ARCHES = ["i386", "amd64"]

ATTR_NORMAL = curses.A_NORMAL
ATTR_SELECTED = curses.A_REVERSE
ATTR_HEADER = curses.A_REVERSE
ATTR_FOOTER = curses.A_NORMAL


@dataclass
class ConfigState:
    arch: str = DEFAULT_ARCH
    ramdisk_prune: bool = True
    ramdisk_full: bool = False
    ramdisk_include_fonts: bool = False
    ramdisk_include_headers: bool = False
    sched_mlfq: bool = True
    sched_mlfq_irq_preempt: bool = False
    sched_mlfq_q0_ms: int = 10
    sched_mlfq_q1_ms: int = 25
    sched_mlfq_q2_ms: int = 50
    sched_mlfq_boost_ms: int = 1000
    selected_apps: Set[str] = None  # type: ignore[assignment]


def bool_to_str(value: bool) -> str:
    return "1" if value else "0"


def str_to_bool(value: str, default: bool) -> bool:
    if value in {"1", "y", "Y", "yes", "true", "on"}:
        return True
    if value in {"0", "n", "N", "no", "false", "off"}:
        return False
    return default


def str_to_int(value: str, default: int, min_value: int, max_value: int) -> int:
    try:
        parsed = int(value.strip(), 10)
    except (AttributeError, ValueError):
        return default
    if parsed < min_value:
        return min_value
    if parsed > max_value:
        return max_value
    return parsed


def discover_apps(repo_root: Path) -> List[str]:
    binaries_dir = repo_root / "testdir" / "binaries"
    if not binaries_dir.is_dir():
        return []

    apps: List[str] = []
    for entry in sorted(binaries_dir.iterdir(), key=lambda p: p.name):
        if not entry.is_file():
            continue
        if entry.name.startswith("."):
            continue
        apps.append(entry.name)
    return apps


def parse_kv_config(path: Path) -> dict:
    values = {}
    if not path.is_file():
        return values

    with path.open("r", encoding="utf-8") as handle:
        for raw in handle:
            line = raw.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, value = line.split("=", 1)
            values[key.strip()] = value.strip()
    return values


def load_state(config_path: Path, apps: List[str]) -> ConfigState:
    values = parse_kv_config(config_path)

    state = ConfigState()
    state.selected_apps = set(apps)

    arch = values.get("ARCH", DEFAULT_ARCH)
    if arch in SUPPORTED_ARCHES:
        state.arch = arch

    state.ramdisk_prune = str_to_bool(
        values.get("CONFIG_INSTALLER_RAMDISK_PRUNE", "1"), True
    )
    state.ramdisk_full = str_to_bool(
        values.get("CONFIG_INSTALLER_RAMDISK_FULL", "0"), False
    )
    state.ramdisk_include_fonts = str_to_bool(
        values.get("CONFIG_INSTALLER_RAMDISK_INCLUDE_FONTS", "0"), False
    )
    state.ramdisk_include_headers = str_to_bool(
        values.get("CONFIG_INSTALLER_RAMDISK_INCLUDE_HEADERS", "0"), False
    )
    state.sched_mlfq = str_to_bool(values.get("CONFIG_SCHED_MLFQ", "1"), True)
    state.sched_mlfq_irq_preempt = str_to_bool(
        values.get("CONFIG_SCHED_MLFQ_IRQ_PREEMPT", "0"), False
    )
    state.sched_mlfq_q0_ms = str_to_int(values.get("CONFIG_SCHED_MLFQ_Q0_MS", "10"), 10, 1, 1000)
    state.sched_mlfq_q1_ms = str_to_int(values.get("CONFIG_SCHED_MLFQ_Q1_MS", "25"), 25, 1, 2000)
    state.sched_mlfq_q2_ms = str_to_int(values.get("CONFIG_SCHED_MLFQ_Q2_MS", "50"), 50, 1, 4000)
    state.sched_mlfq_boost_ms = str_to_int(
        values.get("CONFIG_SCHED_MLFQ_BOOST_MS", "1000"), 1000, 10, 120000
    )

    if state.sched_mlfq_q1_ms < state.sched_mlfq_q0_ms:
        state.sched_mlfq_q1_ms = state.sched_mlfq_q0_ms
    if state.sched_mlfq_q2_ms < state.sched_mlfq_q1_ms:
        state.sched_mlfq_q2_ms = state.sched_mlfq_q1_ms

    selected = values.get("CONFIG_INSTALLER_APPS", "")
    if selected and selected.lower() != "all":
        requested = {item.strip() for item in selected.split(",") if item.strip()}
        state.selected_apps = {app for app in apps if app in requested}

    if "installer" in apps:
        state.selected_apps.add("installer")

    return state


def save_state(config_path: Path, state: ConfigState, apps: List[str]) -> None:
    config_path.parent.mkdir(parents=True, exist_ok=True)

    selected = sorted(app for app in apps if app in state.selected_apps)
    app_list = ",".join(selected)

    lines = [
        "# EYN-OS build configuration",
        "# Generated by: python3 devtools/menuconfig.py",
        "",
        f"ARCH={state.arch}",
        f"CONFIG_INSTALLER_RAMDISK_PRUNE={bool_to_str(state.ramdisk_prune)}",
        f"CONFIG_INSTALLER_RAMDISK_FULL={bool_to_str(state.ramdisk_full)}",
        f"CONFIG_INSTALLER_RAMDISK_INCLUDE_FONTS={bool_to_str(state.ramdisk_include_fonts)}",
        f"CONFIG_INSTALLER_RAMDISK_INCLUDE_HEADERS={bool_to_str(state.ramdisk_include_headers)}",
        f"CONFIG_SCHED_MLFQ={bool_to_str(state.sched_mlfq)}",
        f"CONFIG_SCHED_MLFQ_IRQ_PREEMPT={bool_to_str(state.sched_mlfq_irq_preempt)}",
        f"CONFIG_SCHED_MLFQ_Q0_MS={state.sched_mlfq_q0_ms}",
        f"CONFIG_SCHED_MLFQ_Q1_MS={state.sched_mlfq_q1_ms}",
        f"CONFIG_SCHED_MLFQ_Q2_MS={state.sched_mlfq_q2_ms}",
        f"CONFIG_SCHED_MLFQ_BOOST_MS={state.sched_mlfq_boost_ms}",
        f"CONFIG_INSTALLER_APPS={app_list}",
        "",
    ]

    config_path.write_text("\n".join(lines), encoding="utf-8")


def init_theme(stdscr: curses.window) -> None:
    global ATTR_NORMAL, ATTR_SELECTED, ATTR_HEADER, ATTR_FOOTER

    ATTR_NORMAL = curses.A_NORMAL
    ATTR_SELECTED = curses.A_REVERSE
    ATTR_HEADER = curses.A_REVERSE
    ATTR_FOOTER = curses.A_NORMAL

    try:
        if curses.has_colors():
            curses.start_color()
            try:
                curses.use_default_colors()
            except curses.error:
                pass

            curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
            curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
            curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE)

            ATTR_NORMAL = curses.color_pair(1)
            ATTR_SELECTED = curses.color_pair(2)
            ATTR_HEADER = curses.color_pair(3) | curses.A_BOLD
            ATTR_FOOTER = curses.color_pair(1)

            stdscr.bkgd(" ", ATTR_NORMAL)
            stdscr.attrset(ATTR_NORMAL)
    except curses.error:
        # Keep attribute fallbacks if terminal color setup fails.
        pass


def safe_addnstr(stdscr: curses.window, y: int, x: int, text: str, maxlen: int) -> None:
    height, width = stdscr.getmaxyx()
    if y < 0 or y >= height or x < 0 or x >= width:
        return

    available = width - x
    if available <= 0:
        return

    length = min(maxlen, available)

    # Avoid writing into the lower-right cell; curses treats that as an error.
    if y == height - 1 and x + length >= width:
        length = max(0, width - x - 1)

    if length <= 0:
        return

    try:
        stdscr.addnstr(y, x, text, length)
    except curses.error:
        pass


def safe_hline(stdscr: curses.window, y: int, x: int, ch: int, count: int) -> None:
    height, width = stdscr.getmaxyx()
    if y < 0 or y >= height or x < 0 or x >= width:
        return

    available = width - x
    if available <= 0:
        return

    length = min(count, available)
    if y == height - 1 and x + length >= width:
        length = max(0, width - x - 1)
    if length <= 0:
        return

    try:
        stdscr.hline(y, x, ch, length)
    except curses.error:
        pass


def draw_header(stdscr: curses.window, title: str) -> None:
    height, width = stdscr.getmaxyx()
    if height <= 0 or width <= 0:
        return

    top = f" EYN-OS Build Configuration ({title}) "
    stdscr.attron(ATTR_HEADER)
    safe_addnstr(stdscr, 0, 0, top.ljust(width), width)
    stdscr.attroff(ATTR_HEADER)

    legend = "Arrow keys: navigate  Enter: select  Space: toggle  S: save  Q: back/exit"
    stdscr.attron(ATTR_NORMAL)
    safe_addnstr(stdscr, 1, 0, legend.ljust(width), width)
    stdscr.attroff(ATTR_NORMAL)
    safe_hline(stdscr, 2, 0, ord("-"), width)


def draw_footer(stdscr: curses.window, message: str) -> None:
    height, width = stdscr.getmaxyx()
    safe_hline(stdscr, height - 3, 0, ord("-"), width)
    stdscr.attron(ATTR_FOOTER)
    safe_addnstr(stdscr, height - 2, 0, message.ljust(width), width)
    stdscr.attroff(ATTR_FOOTER)


def ensure_min_size(stdscr: curses.window, min_h: int = 12, min_w: int = 60) -> bool:
    height, width = stdscr.getmaxyx()
    if height >= min_h and width >= min_w:
        return True

    stdscr.clear()
    draw_header(stdscr, "Terminal Too Small")
    msg1 = f"Current size: {width}x{height}. Minimum recommended: {min_w}x{min_h}."
    msg2 = "Resize terminal and press any key to continue, or Q to exit."
    safe_addnstr(stdscr, 5, 2, msg1, max(1, width - 4))
    safe_addnstr(stdscr, 6, 2, msg2, max(1, width - 4))
    stdscr.refresh()

    key = stdscr.getch()
    if key in (ord("q"), ord("Q")):
        raise KeyboardInterrupt
    return False


def run_general_menu(stdscr: curses.window, state: ConfigState) -> None:
    index = 0
    items = [
        "Target architecture",
        "Prune dev-only payload from installer ramdisk",
        "Build full installer stage in ramdisk",
        "Include fonts in installer payload",
        "Include userland headers in installer payload",
        "< Back >",
    ]

    while True:
        if not ensure_min_size(stdscr):
            continue

        stdscr.clear()
        stdscr.attrset(ATTR_NORMAL)
        draw_header(stdscr, "General Setup")

        values = [
            state.arch,
            "*" if state.ramdisk_prune else " ",
            "*" if state.ramdisk_full else " ",
            "*" if state.ramdisk_include_fonts else " ",
            "*" if state.ramdisk_include_headers else " ",
            "",
        ]

        for row, item in enumerate(items):
            y = 4 + row
            marker = ">" if row == index else " "
            if row == 0:
                line = f" {marker} {item:<48} [{values[row]}]"
            elif row < 5:
                line = f" {marker} [{values[row]}] {item}"
            else:
                line = f" {marker} {item}"
            stdscr.attrset(ATTR_SELECTED if row == index else ATTR_NORMAL)
            safe_addnstr(stdscr, y, 2, line, max(1, stdscr.getmaxyx()[1] - 4))
            stdscr.attrset(ATTR_NORMAL)

        draw_footer(stdscr, "Use left/right or Enter on architecture to cycle.")
        stdscr.refresh()

        key = stdscr.getch()
        if key in (ord("q"), ord("Q")):
            return
        if key in (curses.KEY_UP, ord("k")):
            index = (index - 1) % len(items)
        elif key in (curses.KEY_DOWN, ord("j")):
            index = (index + 1) % len(items)
        elif key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("\n"), ord(" ")):
            if index == 0:
                cur = SUPPORTED_ARCHES.index(state.arch)
                step = -1 if key == curses.KEY_LEFT else 1
                if key in (ord("\n"), ord(" ")):
                    step = 1
                state.arch = SUPPORTED_ARCHES[(cur + step) % len(SUPPORTED_ARCHES)]
            elif index == 1:
                state.ramdisk_prune = not state.ramdisk_prune
            elif index == 2:
                state.ramdisk_full = not state.ramdisk_full
            elif index == 3:
                state.ramdisk_include_fonts = not state.ramdisk_include_fonts
            elif index == 4:
                state.ramdisk_include_headers = not state.ramdisk_include_headers
            elif index == 5:
                return


def run_apps_menu(stdscr: curses.window, state: ConfigState, apps: List[str]) -> None:
    if not apps:
        stdscr.clear()
        draw_header(stdscr, "Preinstalled Applications")
        draw_footer(stdscr, "No applications found in testdir/binaries. Press any key.")
        stdscr.refresh()
        stdscr.getch()
        return

    index = 0
    offset = 0

    while True:
        if not ensure_min_size(stdscr):
            continue

        stdscr.clear()
        stdscr.attrset(ATTR_NORMAL)
        draw_header(stdscr, "Preinstalled Applications")

        height, width = stdscr.getmaxyx()
        visible_rows = max(1, height - 8)

        if index < offset:
            offset = index
        if index >= offset + visible_rows:
            offset = index - visible_rows + 1

        end = min(len(apps), offset + visible_rows)
        for row, app in enumerate(apps[offset:end]):
            real_index = offset + row
            selected = app in state.selected_apps
            locked = app == "installer"
            flag = "*" if selected else " "
            suffix = " (required)" if locked else ""
            marker = ">" if real_index == index else " "
            line = f" {marker} [{flag}] {app}{suffix}"
            stdscr.attrset(ATTR_SELECTED if real_index == index else ATTR_NORMAL)
            safe_addnstr(stdscr, 4 + row, 2, line, max(1, width - 4))
            stdscr.attrset(ATTR_NORMAL)

        info = f"Apps: {len(state.selected_apps)}/{len(apps)} selected"
        draw_footer(stdscr, info + "  (A=all, N=none, Space=toggle, Q=back)")
        stdscr.refresh()

        key = stdscr.getch()
        if key in (ord("q"), ord("Q")):
            return
        if key in (curses.KEY_UP, ord("k")):
            index = (index - 1) % len(apps)
        elif key in (curses.KEY_DOWN, ord("j")):
            index = (index + 1) % len(apps)
        elif key == curses.KEY_PPAGE:
            index = max(0, index - visible_rows)
        elif key == curses.KEY_NPAGE:
            index = min(len(apps) - 1, index + visible_rows)
        elif key in (ord("a"), ord("A")):
            state.selected_apps = set(apps)
            if "installer" in apps:
                state.selected_apps.add("installer")
        elif key in (ord("n"), ord("N")):
            state.selected_apps = {"installer"} if "installer" in apps else set()
        elif key in (ord(" "), ord("\n")):
            app = apps[index]
            if app == "installer":
                continue
            if app in state.selected_apps:
                state.selected_apps.remove(app)
            else:
                state.selected_apps.add(app)


def clamp_int(value: int, min_value: int, max_value: int) -> int:
    if value < min_value:
        return min_value
    if value > max_value:
        return max_value
    return value


def run_scheduler_menu(stdscr: curses.window, state: ConfigState) -> None:
    index = 0
    items = [
        "Enable 3-level MLFQ scheduler",
        "Enable IRQ-time preemption hook",
        "Q0 quantum (ms)",
        "Q1 quantum (ms)",
        "Q2 quantum (ms)",
        "Boost interval (ms)",
        "< Back >",
    ]

    while True:
        if not ensure_min_size(stdscr):
            continue

        stdscr.clear()
        stdscr.attrset(ATTR_NORMAL)
        draw_header(stdscr, "Scheduler (MLFQ)")

        values = [
            "*" if state.sched_mlfq else " ",
            "*" if state.sched_mlfq_irq_preempt else " ",
            str(state.sched_mlfq_q0_ms),
            str(state.sched_mlfq_q1_ms),
            str(state.sched_mlfq_q2_ms),
            str(state.sched_mlfq_boost_ms),
            "",
        ]

        for row, item in enumerate(items):
            y = 4 + row
            marker = ">" if row == index else " "
            if row <= 1:
                line = f" {marker} [{values[row]}] {item}"
            elif row <= 5:
                line = f" {marker} {item:<36} [{values[row]}]"
            else:
                line = f" {marker} {item}"
            stdscr.attrset(ATTR_SELECTED if row == index else ATTR_NORMAL)
            safe_addnstr(stdscr, y, 2, line, max(1, stdscr.getmaxyx()[1] - 4))
            stdscr.attrset(ATTR_NORMAL)

        draw_footer(stdscr, "Left/Right adjust values. Enter/Space toggles or increments.")
        stdscr.refresh()

        key = stdscr.getch()
        if key in (ord("q"), ord("Q")):
            return
        if key in (curses.KEY_UP, ord("k")):
            index = (index - 1) % len(items)
            continue
        if key in (curses.KEY_DOWN, ord("j")):
            index = (index + 1) % len(items)
            continue

        if index == 6 and key in (ord("\n"), ord(" ")):
            return

        if index == 0 and key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("\n"), ord(" ")):
            state.sched_mlfq = not state.sched_mlfq
            continue

        if index == 1 and key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("\n"), ord(" ")):
            state.sched_mlfq_irq_preempt = not state.sched_mlfq_irq_preempt
            continue

        if index == 2 and key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("\n"), ord(" ")):
            delta = -1 if key == curses.KEY_LEFT else 1
            state.sched_mlfq_q0_ms = clamp_int(state.sched_mlfq_q0_ms + delta, 1, 1000)
            if state.sched_mlfq_q1_ms < state.sched_mlfq_q0_ms:
                state.sched_mlfq_q1_ms = state.sched_mlfq_q0_ms
            if state.sched_mlfq_q2_ms < state.sched_mlfq_q1_ms:
                state.sched_mlfq_q2_ms = state.sched_mlfq_q1_ms
            continue

        if index == 3 and key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("\n"), ord(" ")):
            delta = -5 if key == curses.KEY_LEFT else 5
            state.sched_mlfq_q1_ms = clamp_int(state.sched_mlfq_q1_ms + delta, 1, 2000)
            if state.sched_mlfq_q1_ms < state.sched_mlfq_q0_ms:
                state.sched_mlfq_q1_ms = state.sched_mlfq_q0_ms
            if state.sched_mlfq_q2_ms < state.sched_mlfq_q1_ms:
                state.sched_mlfq_q2_ms = state.sched_mlfq_q1_ms
            continue

        if index == 4 and key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("\n"), ord(" ")):
            delta = -5 if key == curses.KEY_LEFT else 5
            state.sched_mlfq_q2_ms = clamp_int(state.sched_mlfq_q2_ms + delta, 1, 4000)
            if state.sched_mlfq_q2_ms < state.sched_mlfq_q1_ms:
                state.sched_mlfq_q2_ms = state.sched_mlfq_q1_ms
            continue

        if index == 5 and key in (curses.KEY_LEFT, curses.KEY_RIGHT, ord("\n"), ord(" ")):
            delta = -100 if key == curses.KEY_LEFT else 100
            state.sched_mlfq_boost_ms = clamp_int(state.sched_mlfq_boost_ms + delta, 10, 120000)
            continue


def run_main_menu(stdscr: curses.window, config_path: Path, state: ConfigState, apps: List[str]) -> int:
    curses.curs_set(0)
    stdscr.keypad(True)
    init_theme(stdscr)

    items = [
        "General setup --->",
        "Scheduler (MLFQ) --->",
        "Preinstalled applications --->",
        "Save configuration",
        "Exit",
    ]
    index = 0
    message = f"Config file: {config_path}"

    while True:
        if not ensure_min_size(stdscr):
            continue

        stdscr.clear()
        stdscr.attrset(ATTR_NORMAL)
        draw_header(stdscr, "Main Menu")

        for row, item in enumerate(items):
            y = 5 + row
            marker = ">" if row == index else " "
            line = f" {marker} {item}"
            stdscr.attrset(ATTR_SELECTED if row == index else ATTR_NORMAL)
            safe_addnstr(stdscr, y, 4, line, max(1, stdscr.getmaxyx()[1] - 8))
            stdscr.attrset(ATTR_NORMAL)

        summary = (
            f"ARCH={state.arch}  prune={bool_to_str(state.ramdisk_prune)}  "
            f"full={bool_to_str(state.ramdisk_full)}  mlfq={bool_to_str(state.sched_mlfq)} "
            f"irqpreempt={bool_to_str(state.sched_mlfq_irq_preempt)} "
            f"apps={len(state.selected_apps)}/{len(apps)}"
        )
        draw_footer(stdscr, summary)
        safe_addnstr(stdscr, stdscr.getmaxyx()[0] - 1, 0, message.ljust(stdscr.getmaxyx()[1]), stdscr.getmaxyx()[1])
        stdscr.refresh()

        key = stdscr.getch()
        if key in (curses.KEY_UP, ord("k")):
            index = (index - 1) % len(items)
        elif key in (curses.KEY_DOWN, ord("j")):
            index = (index + 1) % len(items)
        elif key in (ord("q"), ord("Q")):
            return 0
        elif key in (ord("s"), ord("S")):
            save_state(config_path, state, apps)
            message = f"Saved configuration to {config_path}"
        elif key in (ord("\n"), ord(" ")):
            if index == 0:
                run_general_menu(stdscr, state)
            elif index == 1:
                run_scheduler_menu(stdscr, state)
            elif index == 2:
                run_apps_menu(stdscr, state, apps)
            elif index == 3:
                save_state(config_path, state, apps)
                message = f"Saved configuration to {config_path}"
            elif index == 4:
                return 0


def main() -> int:
    repo_root = Path(__file__).resolve().parent.parent
    config_path = repo_root / ".eynosconfig"

    if len(sys.argv) > 1:
        config_path = Path(sys.argv[1]).resolve()

    apps = discover_apps(repo_root)
    state = load_state(config_path, apps)

    if state.selected_apps is None:
        state.selected_apps = set(apps)

    if "TERM" not in os.environ:
        print("error: TERM is not set; menuconfig requires a real terminal")
        return 1

    if not sys.stdin.isatty() or not sys.stdout.isatty():
        print("error: menuconfig requires an interactive TTY")
        return 1

    try:
        return curses.wrapper(lambda stdscr: run_main_menu(stdscr, config_path, state, apps))
    except KeyboardInterrupt:
        return 0
    except curses.error as exc:
        print(f"error: curses UI failed: {exc}")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
