from __future__ import annotations

import curses
import re
import textwrap
from collections import defaultdict
from dataclasses import dataclass
from typing import Callable

from .assets import banner_text
from .models import Scenario, TaskDefinition, TaskResult
from .status import StatusItem
from .storage import ScenarioStore
from .system_info import SystemInfo


@dataclass
class AppSelection:
    task_keys: list[str]
    scenario_name: str | None = None


class Palette:
    TITLE = 1
    HEADER = 2
    PANEL = 3
    SELECTED = 4
    SUCCESS = 5
    ERROR = 6
    MUTED = 7
    HIGHLIGHT = 8
    CATEGORY = 9


def _setup_colors() -> None:
    if not curses.has_colors():
        return
    curses.start_color()
    curses.use_default_colors()
    curses.init_pair(Palette.TITLE, curses.COLOR_CYAN, -1)
    curses.init_pair(Palette.HEADER, curses.COLOR_YELLOW, -1)
    curses.init_pair(Palette.PANEL, curses.COLOR_WHITE, curses.COLOR_BLUE)
    curses.init_pair(Palette.SELECTED, curses.COLOR_BLACK, curses.COLOR_CYAN)
    curses.init_pair(Palette.SUCCESS, curses.COLOR_GREEN, -1)
    curses.init_pair(Palette.ERROR, curses.COLOR_RED, -1)
    curses.init_pair(Palette.MUTED, curses.COLOR_BLUE, -1)
    curses.init_pair(Palette.HIGHLIGHT, curses.COLOR_BLACK, curses.COLOR_YELLOW)
    curses.init_pair(Palette.CATEGORY, curses.COLOR_MAGENTA, -1)


class SecureCheckTUI:
    def __init__(
        self,
        system: SystemInfo,
        tasks: list[TaskDefinition],
        store: ScenarioStore,
        *,
        status_provider: Callable[[], list[StatusItem]],
        initial_selected: set[str] | None = None,
        initial_scenario_name: str | None = None,
        initial_message: str | None = None,
    ) -> None:
        self.system = system
        self.tasks = tasks
        self.store = store
        self.status_provider = status_provider
        self.index = 0
        self.message = initial_message or "Sélectionnez les tâches puis lancez avec 'r'."
        self.selected = initial_selected if initial_selected is not None else {task.key for task in tasks if task.default_selected}
        self.scenario_name = initial_scenario_name
        self.banner_lines = banner_text().splitlines()
        self.status_items = self.status_provider()

    def run(self) -> AppSelection | None:
        return curses.wrapper(self._main)

    def _main(self, stdscr: curses.window) -> AppSelection | None:
        curses.curs_set(0)
        stdscr.keypad(True)
        _setup_colors()

        while True:
            self._draw(stdscr)
            key = stdscr.getch()

            if key in (ord("q"), 27):
                return None
            if key in (curses.KEY_UP, ord("k")):
                self.index = max(0, self.index - 1)
            elif key in (curses.KEY_DOWN, ord("j")):
                self.index = min(len(self.tasks) - 1, self.index + 1)
            elif key == ord(" "):
                current = self.tasks[self.index].key
                if current in self.selected:
                    self.selected.remove(current)
                else:
                    self.selected.add(current)
            elif key == ord("a"):
                baseline = self.store.get("baseline_workstation")
                if baseline:
                    self.selected = set(baseline.task_keys)
                    self.scenario_name = baseline.name
                    self.message = "Scénario baseline_workstation chargé."
            elif key == ord("s"):
                self._save_current(stdscr)
            elif key == ord("l"):
                self._load_scenario(stdscr)
            elif key == ord("d"):
                self._show_dashboard(stdscr)
            elif key == ord("x"):
                self._delete_scenario(stdscr)
            elif key == ord("r"):
                if not self.selected:
                    self.message = "Aucune tâche sélectionnée."
                    continue
                return AppSelection(
                    task_keys=[task.key for task in self.tasks if task.key in self.selected],
                    scenario_name=self.scenario_name,
                )
            elif key == ord("?"):
                self._show_help(stdscr)

    def _draw(self, stdscr: curses.window) -> None:
        stdscr.erase()
        height, width = stdscr.getmaxyx()
        header_row = 1
        for line in self.banner_lines[: min(6, max(0, height - 8))]:
            stdscr.addnstr(header_row, 2, line, width - 4, curses.color_pair(Palette.TITLE) | curses.A_BOLD)
            header_row += 1

        scenario_hint = f" | scénario={self.scenario_name}" if self.scenario_name else ""
        header = f"SecureCheck | {self.system.pretty_name} | user={self.system.target_user} | pkg={self.system.package_manager}{scenario_hint}"
        stdscr.addnstr(header_row, 2, header, width - 4, curses.color_pair(Palette.HEADER) | curses.A_BOLD)
        stdscr.addnstr(
            header_row + 1,
            2,
            "↑↓ naviguer  Espace cocher  s sauver  l charger  d état  x supprimer  r exécuter  q quitter",
            width - 4,
            curses.color_pair(Palette.MUTED),
        )
        stdscr.hline(header_row + 2, 1, curses.ACS_HLINE, width - 2)

        list_top = header_row + 4
        desc_height = 5
        status_width = 40 if width >= 120 else 0
        task_width = width - 4 - status_width - (2 if status_width else 0)
        left_x = 1
        right_x = left_x + task_width + 2 if status_width else 0
        content_height = max(8, height - list_top - desc_height - 2)
        self._draw_box(stdscr, list_top, left_x, content_height, task_width, "Tâches")
        self._draw_task_list(stdscr, list_top + 1, left_x + 1, content_height - 2, task_width - 2)
        if status_width:
            self._draw_box(stdscr, list_top, right_x, content_height, status_width, "Etat")
            self._draw_status_panel(stdscr, list_top + 1, right_x + 1, content_height - 2, status_width - 2)

        desc_top = list_top + content_height + 1
        self._draw_box(stdscr, desc_top, 1, desc_height, width - 2, "Détail")
        current = self.tasks[self.index]
        count_selected = len(self.selected)
        total_ok = sum(1 for item in self.status_items if item.ok)
        total_ko = len(self.status_items) - total_ok
        summary = f"Sélection: {count_selected}/{len(self.tasks)}  |  Etat: {total_ok} OK / {total_ko} KO"
        stdscr.addnstr(desc_top + 1, 3, summary, width - 8, curses.color_pair(Palette.HEADER) | curses.A_BOLD)
        for offset, line in enumerate(textwrap.wrap(current.description, width - 8)[:2], start=desc_top + 2):
            stdscr.addnstr(offset, 3, line, width - 8)
        stdscr.addnstr(height - 1, 2, self.message, width - 4, curses.color_pair(Palette.MUTED) | curses.A_BOLD)
        stdscr.refresh()

    def _draw_box(self, stdscr: curses.window, top: int, left: int, height: int, width: int, title: str) -> None:
        stdscr.attron(curses.color_pair(Palette.PANEL))
        stdscr.addch(top, left, curses.ACS_ULCORNER)
        stdscr.hline(top, left + 1, curses.ACS_HLINE, width - 2)
        stdscr.addch(top, left + width - 1, curses.ACS_URCORNER)
        for row in range(top + 1, top + height - 1):
            stdscr.addch(row, left, curses.ACS_VLINE)
            stdscr.addch(row, left + width - 1, curses.ACS_VLINE)
        stdscr.addch(top + height - 1, left, curses.ACS_LLCORNER)
        stdscr.hline(top + height - 1, left + 1, curses.ACS_HLINE, width - 2)
        stdscr.addch(top + height - 1, left + width - 1, curses.ACS_LRCORNER)
        stdscr.attroff(curses.color_pair(Palette.PANEL))
        stdscr.addnstr(top, left + 2, f" {title} ", width - 4, curses.color_pair(Palette.HEADER) | curses.A_BOLD)

    def _draw_task_list(self, stdscr: curses.window, top: int, left: int, height: int, width: int) -> None:
        window_start = max(0, min(self.index - (height // 2), max(0, len(self.tasks) - height)))
        visible_tasks = self.tasks[window_start : window_start + height]
        for offset in range(height):
            stdscr.addnstr(top + offset, left, " " * width, width)
        for row, task in enumerate(visible_tasks):
            y = top + row
            selected = task.key in self.selected
            current = self.tasks[self.index].key == task.key
            marker = "✓" if selected else " "
            category = f"[{task.category}]"
            base_attr = curses.color_pair(Palette.SELECTED) | curses.A_BOLD if current else curses.A_NORMAL
            stdscr.addnstr(y, left, " " * width, width, base_attr)
            led_attr = curses.color_pair(Palette.SUCCESS if selected else Palette.MUTED) | curses.A_BOLD
            stdscr.addnstr(y, left, "●", 1, led_attr | (curses.A_REVERSE if current else 0))
            stdscr.addnstr(y, left + 2, f"[{marker}]", 3, base_attr | (curses.color_pair(Palette.SUCCESS) if selected else 0))
            stdscr.addnstr(y, left + 6, task.label, max(1, width - 22), base_attr)
            stdscr.addnstr(y, left + width - min(18, len(category) + 1), category, min(18, width - 1), curses.color_pair(Palette.CATEGORY) | (curses.A_BOLD if current else 0))

    def _draw_status_panel(self, stdscr: curses.window, top: int, left: int, height: int, width: int) -> None:
        self.status_items = self.status_provider()
        items = self.status_items[:height]
        for offset in range(height):
            stdscr.addnstr(top + offset, left, " " * width, width)
        for row, item in enumerate(items):
            color = curses.color_pair(Palette.SUCCESS if item.ok else Palette.ERROR) | curses.A_BOLD
            stdscr.addnstr(top + row, left, "●", 1, color)
            line = f" {item.label:<16} {item.detail}"
            stdscr.addnstr(top + row, left + 2, line, width - 2)

    def _prompt(self, stdscr: curses.window, prompt: str) -> str | None:
        height, width = stdscr.getmaxyx()
        curses.echo()
        curses.curs_set(1)
        stdscr.move(height - 1, 0)
        stdscr.clrtoeol()
        stdscr.addnstr(height - 1, 2, prompt, width - 4, curses.color_pair(Palette.HIGHLIGHT) | curses.A_BOLD)
        value = stdscr.getstr(height - 1, min(len(prompt) + 2, width - 2), 60).decode("utf-8").strip()
        curses.noecho()
        curses.curs_set(0)
        return value or None

    def _pick_scenario(self, stdscr: curses.window, *, deletable_only: bool = False) -> Scenario | None:
        scenarios = self.store.list_all()
        if deletable_only:
            scenarios = [scenario for scenario in scenarios if not scenario.builtin]
        if not scenarios:
            self.message = "Aucun scénario disponible."
            return None

        index = 0
        while True:
            stdscr.erase()
            _setup_colors()
            height, width = stdscr.getmaxyx()
            self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Scénarios")
            stdscr.addnstr(1, 3, "Entrée valider | q retour", width - 6, curses.color_pair(Palette.MUTED))
            for line_no, scenario in enumerate(scenarios[: height - 4], start=2):
                prefix = ">" if line_no - 2 == index else " "
                kind = "builtin" if scenario.builtin else "user"
                attr = curses.color_pair(Palette.SELECTED) | curses.A_BOLD if line_no - 2 == index else curses.A_NORMAL
                stdscr.addnstr(line_no + 1, 3, f"{prefix} {scenario.name} [{kind}] - {scenario.description}", width - 6, attr)
            stdscr.refresh()
            key = stdscr.getch()
            if key in (ord("q"), 27):
                return None
            if key in (curses.KEY_UP, ord("k")):
                index = max(0, index - 1)
            elif key in (curses.KEY_DOWN, ord("j")):
                index = min(len(scenarios) - 1, index + 1)
            elif key in (10, 13, curses.KEY_ENTER):
                return scenarios[index]

    def _save_current(self, stdscr: curses.window) -> None:
        name = self._prompt(stdscr, "Nom du scénario: ")
        if not name:
            self.message = "Sauvegarde annulée."
            return
        description = self._prompt(stdscr, "Description courte: ") or ""
        self.store.save(Scenario(name=name, description=description, task_keys=sorted(self.selected)))
        self.scenario_name = name
        self.message = f"Scénario '{name}' enregistré."

    def _load_scenario(self, stdscr: curses.window) -> None:
        scenario = self._pick_scenario(stdscr)
        if not scenario:
            self.message = "Chargement annulé."
            return
        self.selected = set(scenario.task_keys)
        self.scenario_name = scenario.name
        self.message = f"Scénario '{scenario.name}' chargé."

    def _delete_scenario(self, stdscr: curses.window) -> None:
        scenario = self._pick_scenario(stdscr, deletable_only=True)
        if not scenario:
            self.message = "Suppression annulée."
            return
        if self.store.delete(scenario.name):
            self.message = f"Scénario '{scenario.name}' supprimé."
        else:
            self.message = "Impossible de supprimer le scénario."

    def _show_help(self, stdscr: curses.window) -> None:
        lines = [
            "SecureCheck - Aide rapide",
            "",
            "Chaque ligne correspond à une action automatisable.",
            "Les scénarios permettent d'enregistrer un lot réutilisable.",
            "Vous pouvez précharger un scénario puis ajuster les cases avant exécution.",
            "La touche d affiche le tableau d'état du système avec indicateurs rouge/vert.",
            "Après exécution, l'application affiche un résumé puis revient au menu.",
            "",
            "Appuyez sur une touche pour revenir.",
        ]
        stdscr.erase()
        _setup_colors()
        height, width = stdscr.getmaxyx()
        self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Aide")
        for index, line in enumerate(lines[: height - 1]):
            stdscr.addnstr(index + 1, 3, line, width - 6, curses.color_pair(Palette.HEADER) if index == 0 else curses.A_NORMAL)
        stdscr.refresh()
        stdscr.getch()

    def _show_dashboard(self, stdscr: curses.window) -> None:
        self.status_items = self.status_provider()
        groups: dict[str, list[StatusItem]] = defaultdict(list)
        for item in self.status_items:
            groups[item.category].append(item)

        _setup_colors()

        stdscr.erase()
        height, width = stdscr.getmaxyx()
        self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Tableau d'état")
        stdscr.addnstr(1, 3, "Vert = OK | Rouge = manquant/inactif | Appuyez sur une touche", width - 6, curses.color_pair(Palette.MUTED))
        row = 3
        for category, items in groups.items():
            if row >= height - 1:
                break
            stdscr.addnstr(row, 3, f"[{category}]", width - 6, curses.color_pair(Palette.CATEGORY) | curses.A_BOLD)
            row += 1
            for item in items:
                if row >= height - 1:
                    break
                color = curses.color_pair(Palette.SUCCESS if item.ok else Palette.ERROR)
                stdscr.addstr(row, 3, "●", color | curses.A_BOLD)
                stdscr.addnstr(row, 5, f"{item.label:<18} {item.detail}", width - 8)
                row += 1
            row += 1
        stdscr.refresh()
        stdscr.getch()


class RunSummaryTUI:
    ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")

    def __init__(self, results: list[TaskResult], status_items: list[StatusItem], run_log_path: str) -> None:
        self.results = results
        self.status_items = status_items
        self.run_log_path = run_log_path
        self.scroll_offset = 0

    @classmethod
    def _clean(cls, text: str) -> str:
        return cls.ANSI_RE.sub("", text)

    def run(self) -> None:
        curses.wrapper(self._main)

    def _main(self, stdscr: curses.window) -> None:
        curses.curs_set(0)
        stdscr.keypad(True)
        _setup_colors()
        while True:
            self._draw(stdscr)
            key = stdscr.getch()
            if key in (ord("q"), 27, 10, 13, ord("m"), ord(" ")):
                return
            if key == curses.KEY_UP and self.scroll_offset > 0:
                self.scroll_offset -= 1
            elif key == curses.KEY_DOWN:
                self.scroll_offset += 1

    def _draw(self, stdscr: curses.window) -> None:
        stdscr.erase()
        height, width = stdscr.getmaxyx()
        ok_count = sum(1 for result in self.results if result.success)
        ko_count = len(self.results) - ok_count
        score_lines: list[str] = []
        notif_lines: list[str] = []
        entries: list[tuple[str, int]] = []
        entries.append((f"OK: {ok_count} | ECHEC: {ko_count} | Appuie sur une touche pour revenir", curses.color_pair(Palette.HEADER) | curses.A_BOLD))
        for result in self.results:
            status = "OK" if result.success else "ECHEC"
            color = curses.color_pair(Palette.SUCCESS if result.success else Palette.ERROR)
            entries.append((f"{status:<4} {result.label} ({result.duration_seconds:.1f}s)", color | curses.A_BOLD))
            for detail in result.details:
                clean = self._clean(detail)
                if clean.startswith("Score Lynis") or clean.startswith("Hardening index"):
                    score_lines.append(clean)
                    continue
                if clean.startswith("Modifications") or clean.strip().startswith("•"):
                    notif_lines.append(clean)
                    continue
                wrapped = textwrap.wrap(clean, width - 9) or [""]
                for line in wrapped:
                    entries.append((f"  - {line}", 0))
            if result.error:
                entries.append((f"  - {result.error}", curses.color_pair(Palette.ERROR)))
        if score_lines:
            entries.insert(1, ("Lynis", curses.color_pair(Palette.CATEGORY) | curses.A_BOLD))
            for idx, line in enumerate(score_lines, start=2):
                entries.insert(idx, (f"  {line}", curses.color_pair(Palette.SUCCESS)))
        entries.append((("", 0)))
        entries.append(("Etat synthétique:", curses.color_pair(Palette.CATEGORY) | curses.A_BOLD))
        for item in self.status_items:
            attr = curses.color_pair(Palette.SUCCESS if item.ok else Palette.ERROR) | curses.A_BOLD
            entries.append((f"● [{item.category}] {item.label}: {item.detail}", attr))
        if notif_lines:
            entries.append((("", 0)))
            entries.append(("Modifications recommandées:", curses.color_pair(Palette.ERROR) | curses.A_BOLD))
            for line in notif_lines:
                clean = self._clean(line)
                bullet = "•" if clean.strip().startswith("•") else "-"
                entries.append((f"  {bullet} {clean.lstrip('• ').strip()}", curses.color_pair(Palette.MUTED)))
        entries.append(("", 0))
        entries.append((f"Log: {self.run_log_path}", curses.color_pair(Palette.MUTED)))

        available = height - 4
        max_offset = max(0, len(entries) - available)
        self.scroll_offset = min(max(self.scroll_offset, 0), max_offset)
        visible = entries[self.scroll_offset : self.scroll_offset + available]

        self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Résumé d'exécution")
        for idx, (line, attr) in enumerate(visible):
            stdscr.addnstr(2 + idx, 3, line, width - 6, attr)
        if max_offset:
            bar_pos = int((self.scroll_offset / max_offset) * (available - 1)) if max_offset else 0
            stdscr.addch(2 + min(bar_pos, available - 1), width - 3, curses.ACS_CKBOARD)
    def _draw_box(self, stdscr: curses.window, top: int, left: int, height: int, width: int, title: str) -> None:
        stdscr.attron(curses.color_pair(Palette.PANEL))
        stdscr.addch(top, left, curses.ACS_ULCORNER)
        stdscr.hline(top, left + 1, curses.ACS_HLINE, width - 2)
        stdscr.addch(top, left + width - 1, curses.ACS_URCORNER)
        for row in range(top + 1, top + height - 1):
            stdscr.addch(row, left, curses.ACS_VLINE)
            stdscr.addch(row, left + width - 1, curses.ACS_VLINE)
        stdscr.addch(top + height - 1, left, curses.ACS_LLCORNER)
        stdscr.hline(top + height - 1, left + 1, curses.ACS_HLINE, width - 2)
        stdscr.addch(top + height - 1, left + width - 1, curses.ACS_LRCORNER)
        stdscr.attroff(curses.color_pair(Palette.PANEL))
        stdscr.addnstr(top, left + 2, f" {title} ", width - 4, curses.color_pair(Palette.HEADER) | curses.A_BOLD)
