"""
Curses-based VT220 renderer for the NEXUS AUTOPARTS console application.

Implements :class:`BaseRenderer` with a green-on-black aesthetic inspired
by classic Pick/UNIX VT220 terminals.  All drawing is done through Python's
built-in :mod:`curses` library.
"""

import curses
import os

# Reduce ESC key delay from default 1000ms to 25ms.
# Must be set BEFORE curses.initscr() is called.
os.environ.setdefault('ESCDELAY', '25')

from console.config import COLORS_VT220
from console.renderers.base import BaseRenderer
from console.utils.formatting import pad_right, truncate

# ── Colour-name-to-curses mapping ────────────────────────────────────

_CURSES_COLORS = {
    "black":   curses.COLOR_BLACK,
    "red":     curses.COLOR_RED,
    "green":   curses.COLOR_GREEN,
    "yellow":  curses.COLOR_YELLOW,
    "blue":    curses.COLOR_BLUE,
    "magenta": curses.COLOR_MAGENTA,
    "cyan":    curses.COLOR_CYAN,
    "white":   curses.COLOR_WHITE,
}

# Box-drawing characters
_BOX_H  = "\u2500"   # ─
_BOX_V  = "\u2502"   # │
_BOX_TL = "\u250c"   # ┌
_BOX_TR = "\u2510"   # ┐
_BOX_BL = "\u2514"   # └
_BOX_BR = "\u2518"   # ┘


class CursesRenderer(BaseRenderer):
    """Full curses implementation of the VT220 green-on-black renderer."""

    def __init__(self):
        self._screen = None
        self._color_pairs: dict[str, int] = {}
        self._size_cache: tuple = (24, 80)

    # ── Lifecycle ────────────────────────────────────────────────────

    def init_screen(self):
        """Set up curses: raw mode, no echo, hidden cursor, colours."""
        self._screen = curses.initscr()
        curses.noecho()
        curses.cbreak()
        curses.curs_set(0)
        self._screen.keypad(True)
        self._init_colors()

    def cleanup(self):
        """Restore the terminal to a usable state."""
        if self._screen is None:
            return
        try:
            curses.nocbreak()
            self._screen.keypad(False)
            curses.echo()
        except curses.error:
            pass
        curses.endwin()
        self._screen = None

    # ── Screen queries ───────────────────────────────────────────────

    def get_size(self) -> tuple:
        """Return ``(height, width)`` with cached value per render cycle."""
        return self._size_cache

    # ── Primitive operations ─────────────────────────────────────────

    def clear(self):
        self._size_cache = self._screen.getmaxyx()
        self._screen.erase()

    def refresh(self):
        self._screen.refresh()

    def get_key(self) -> int:
        return self._screen.getch()

    # ── Colour helpers ───────────────────────────────────────────────

    def _init_colors(self):
        """Initialise curses colour pairs from ``COLORS_VT220``."""
        curses.start_color()
        curses.use_default_colors()
        for idx, (name, (fg, bg)) in enumerate(COLORS_VT220.items(), start=1):
            curses.init_pair(idx, _CURSES_COLORS[fg], _CURSES_COLORS[bg])
            self._color_pairs[name] = idx

    def _attr(self, style: str) -> int:
        """Return the curses attribute for a named style.

        Falls back to the *normal* pair if *style* is unknown.
        """
        pair_id = self._color_pairs.get(style,
                                        self._color_pairs.get("normal", 1))
        attr = curses.color_pair(pair_id)
        if style in ("header", "title"):
            attr |= curses.A_BOLD
        return attr

    # ── Safe drawing helpers ─────────────────────────────────────────

    def _safe_addstr(self, row, col, text, attr=None):
        """Write *text* at (row, col), silently ignoring edge overflows."""
        if attr is None:
            attr = self._attr("normal")
        h, w = self.get_size()
        if row < 0 or row >= h or col >= w:
            return
        # Truncate to fit within the screen width
        max_chars = w - col
        if max_chars <= 0:
            return
        text = text[:max_chars]
        try:
            self._screen.addstr(row, col, text, attr)
        except curses.error:
            # Writing to the bottom-right corner raises an error after
            # the character is actually drawn.  Safe to ignore.
            pass

    def _hline(self, row, col, width, char=_BOX_H, style="border"):
        """Draw a horizontal line of *char* across *width* columns."""
        self._safe_addstr(row, col, char * width, self._attr(style))

    # ── High-level widgets ───────────────────────────────────────────

    def draw_header(self, title, subtitle=''):
        h, w = self.get_size()
        attr = self._attr("header")
        # Row 0: title (left) + subtitle (right)
        header_line = pad_right(title, w)
        if subtitle:
            sub = subtitle[:w - len(title) - 1]
            header_line = (title
                           + " " * max(w - len(title) - len(sub), 0)
                           + sub)
            header_line = pad_right(header_line, w)
        self._safe_addstr(0, 0, header_line, attr | curses.A_BOLD)
        # Row 1: separator
        self._hline(1, 0, w)

    def draw_footer(self, key_labels):
        h, w = self.get_size()
        if h < 3:
            return
        # Row h-2: separator
        self._hline(h - 2, 0, w)
        # Row h-1: key labels
        attr = self._attr("footer")
        parts = [f"{k}={d}" for k, d in key_labels]
        line = "  ".join(parts)
        self._safe_addstr(h - 1, 0, pad_right(line, w), attr)

    def draw_menu(self, items, selected_index=0, title=''):
        h, w = self.get_size()

        # Calculate menu dimensions for centering
        item_count = len(items)
        # Find widest label for box sizing
        max_label = 0
        for num, label in items:
            if num != "---" and num != "\u2500":
                max_label = max(max_label, len(f"  {num}. {label}  "))
        box_w = max(max_label + 8, 44)
        box_w = min(box_w, w - 4)
        box_h = item_count + 4  # top/bottom border + title + blank line
        if title:
            box_h += 2

        # Center the box vertically and horizontally
        start_row = max((h - box_h) // 2 - 1, 2)
        start_col = max((w - box_w) // 2, 1)

        # Draw box
        self.draw_box(start_row, start_col, box_h, box_w, "")

        row = start_row + 1
        if title:
            # Title centered inside the box
            title_col = start_col + max((box_w - len(title)) // 2, 2)
            self._safe_addstr(row, title_col, title,
                              self._attr("title"))
            row += 1
            self._hline(row, start_col + 1, box_w - 2)
            row += 1

        # Menu items inside the box
        inner_left = start_col + 3
        inner_w = box_w - 6

        visible = box_h - (row - start_row) - 1
        offset = 0
        if selected_index >= visible:
            offset = selected_index - visible + 1

        drawn = 0
        for idx, (num, label) in enumerate(items):
            if drawn >= visible:
                break
            if idx < offset:
                continue

            # Separator
            if num == "\u2500" or num == "---":
                self._hline(row, start_col + 1, box_w - 2)
                row += 1
                drawn += 1
                continue

            marker = "\u25b8 " if idx == selected_index else "  "
            text = f"{marker}{num}. {label}"
            style = "highlight" if idx == selected_index else "normal"
            self._safe_addstr(row, inner_left, pad_right(text, inner_w),
                              self._attr(style))
            row += 1
            drawn += 1

    def draw_table(self, headers, rows, widths, page_info=None,
                   selected_row=-1):
        h, w = self.get_size()
        start_row = 3

        # Header row
        header_cells = [pad_right(hdr, wd) for hdr, wd in zip(headers, widths)]
        header_text = "  # " + " \u2502 ".join(header_cells)
        self._safe_addstr(start_row, 0, pad_right(header_text, w),
                          self._attr("title"))
        # Separator
        self._hline(start_row + 1, 0, w)

        visible = h - start_row - 5  # room for header, sep, footer
        if visible < 1:
            return

        for i, row_data in enumerate(rows):
            if i >= visible:
                break
            row_num = start_row + 2 + i
            row_idx_str = pad_right(str(i + 1), 3)
            cells = [pad_right(str(v), wd) for v, wd in zip(row_data, widths)]
            line = f" {row_idx_str}\u2502 " + " \u2502 ".join(cells)
            style = "highlight" if i == selected_row else "normal"
            self._safe_addstr(row_num, 0, pad_right(line, w),
                              self._attr(style))

        # Page info
        if page_info:
            info_row = start_row + 2 + min(len(rows), visible)
            page = page_info.get("page", 1)
            total = page_info.get("total_pages", 1)
            total_rows = page_info.get("total_rows", len(rows))
            info_text = (f"  Pagina {page}/{total}"
                         f"  ({total_rows} registros)")
            self._safe_addstr(info_row, 0, info_text, self._attr("info"))

    def draw_detail(self, fields, title=''):
        h, w = self.get_size()
        start_row = 3

        if title:
            self._safe_addstr(start_row, 2, title, self._attr("title"))
            self._hline(start_row + 1, 2, w - 4)
            start_row += 3

        # Determine max label width for alignment
        max_label = max((len(lbl) for lbl, _ in fields), default=10)
        dot_total = max_label + 4  # label + dots

        for i, (label, value) in enumerate(fields):
            row = start_row + i
            if row >= h - 3:
                break
            dots = "." * (dot_total - len(label))
            label_part = f"  {label}{dots}: "
            self._safe_addstr(row, 0, label_part,
                              self._attr("field_label"))
            self._safe_addstr(row, len(label_part), str(value),
                              self._attr("field_value"))

    def draw_form(self, fields, focused_index=0, title=''):
        h, w = self.get_size()
        start_row = 3

        if title:
            self._safe_addstr(start_row, 2, title, self._attr("title"))
            self._hline(start_row + 1, 2, w - 4)
            start_row += 3

        max_label = max((len(f.get("label", "")) for f in fields),
                        default=10)
        dot_total = max_label + 4

        for i, field in enumerate(fields):
            row = start_row + i * 2  # space between fields
            if row >= h - 3:
                break

            label = field.get("label", "")
            value = field.get("value", "")
            fw = field.get("width", 20)
            hint = field.get("hint", "")

            dots = "." * (dot_total - len(label))
            num_str = f"{i + 1}. "
            label_part = f"  {num_str}{label}{dots}: "

            self._safe_addstr(row, 0, label_part,
                              self._attr("field_label"))

            # Editable field value in brackets
            style = "field_active" if i == focused_index else "field_value"
            display_val = pad_right(str(value), fw)
            field_text = f"[{display_val}]"
            self._safe_addstr(row, len(label_part), field_text,
                              self._attr(style))

            # Optional hint
            if hint:
                hint_col = len(label_part) + len(field_text) + 2
                self._safe_addstr(row, hint_col, hint,
                                  self._attr("info"))

    def draw_filter_list(self, items, filter_text, selected_index,
                         title=''):
        h, w = self.get_size()
        start_row = 3

        if title:
            self._safe_addstr(start_row, 2, title, self._attr("title"))
            start_row += 1

        # Separator
        self._hline(start_row, 2, w - 4)
        start_row += 1

        # Filter input
        prompt = "Filtro: "
        self._safe_addstr(start_row, 2, prompt,
                          self._attr("field_label"))
        self._safe_addstr(start_row, 2 + len(prompt),
                          filter_text + "_",
                          self._attr("field_active"))
        start_row += 1

        # Separator
        self._hline(start_row, 2, w - 4)
        start_row += 1

        # Scrollable list
        visible = h - start_row - 4
        if visible < 1:
            return

        offset = 0
        if selected_index >= visible:
            offset = selected_index - visible + 1

        drawn = 0
        for idx, (num, label) in enumerate(items):
            if drawn >= visible:
                break
            if idx < offset:
                continue
            row = start_row + drawn
            marker = "\u25b8 " if idx == selected_index else "  "
            text = f"{marker}{num}. {label}"
            style = "highlight" if idx == selected_index else "normal"
            self._safe_addstr(row, 2, pad_right(text, w - 4),
                              self._attr(style))
            drawn += 1

        # Count at bottom
        count_row = start_row + min(drawn, visible)
        count_text = f"  {len(items)} elementos"
        self._safe_addstr(count_row, 2, count_text, self._attr("info"))

    def draw_comparison(self, columns, title=''):
        h, w = self.get_size()
        start_row = 3

        if title:
            self._safe_addstr(start_row, 2, title, self._attr("title"))
            self._hline(start_row + 1, 2, w - 4)
            start_row += 3

        n_cols = len(columns)
        if n_cols == 0:
            return

        # Determine label width from the first column's row labels
        all_labels = []
        for col in columns:
            for lbl, _ in col.get("rows", []):
                all_labels.append(lbl)
        label_w = max((len(l) for l in all_labels), default=8) + 2

        # Available width for data columns
        avail = w - label_w - 4
        col_w = max(avail // n_cols, 10)

        # Header row
        header_line = pad_right("", label_w)
        for col in columns:
            header_line += " \u2502 " + pad_right(col.get("header", ""), col_w)
        self._safe_addstr(start_row, 2, header_line, self._attr("title"))

        # Separator
        self._hline(start_row + 1, 2, w - 4)

        # Data rows — use the first column's labels as the canonical set
        if not columns[0].get("rows"):
            return
        n_rows = len(columns[0]["rows"])
        for i in range(n_rows):
            row = start_row + 2 + i
            if row >= h - 3:
                break
            lbl = columns[0]["rows"][i][0] if i < len(columns[0]["rows"]) else ""
            line = pad_right(lbl, label_w)
            for col in columns:
                rows_data = col.get("rows", [])
                val = rows_data[i][1] if i < len(rows_data) else ""
                line += " \u2502 " + pad_right(str(val), col_w)
            self._safe_addstr(row, 2, line, self._attr("normal"))

    # ── Low-level drawing ────────────────────────────────────────────

    def draw_text(self, row, col, text, style='normal'):
        self._safe_addstr(row, col, text, self._attr(style))

    def draw_box(self, top, left, height, width, title=''):
        if height < 2 or width < 2:
            return
        attr = self._attr("border")

        # Top border
        top_line = _BOX_TL + _BOX_H * (width - 2) + _BOX_TR
        if title:
            t = truncate(title, width - 4)
            top_line = (_BOX_TL + _BOX_H + t
                        + _BOX_H * (width - 3 - len(t)) + _BOX_TR)
        self._safe_addstr(top, left, top_line, attr)

        # Side borders
        for r in range(1, height - 1):
            self._safe_addstr(top + r, left, _BOX_V, attr)
            self._safe_addstr(top + r, left + width - 1, _BOX_V, attr)

        # Bottom border
        bottom_line = _BOX_BL + _BOX_H * (width - 2) + _BOX_BR
        self._safe_addstr(top + height - 1, left, bottom_line, attr)

    # ── Dialogs ──────────────────────────────────────────────────────

    def show_message(self, text, msg_type='info') -> bool:
        h, w = self.get_size()
        lines = text.split("\n")
        box_w = max(max((len(l) for l in lines), default=20) + 6, 30)
        box_h = len(lines) + 4
        top = max((h - box_h) // 2, 0)
        left = max((w - box_w) // 2, 0)

        style = "error" if msg_type == "error" else "info"
        self.draw_box(top, left, box_h, box_w)

        # Fill interior with spaces
        interior_attr = self._attr(style)
        for r in range(1, box_h - 1):
            self._safe_addstr(top + r, left + 1,
                              " " * (box_w - 2), interior_attr)

        # Draw message lines
        for i, line in enumerate(lines):
            x = left + max((box_w - len(line)) // 2, 2)
            self._safe_addstr(top + 1 + i, x, line, interior_attr)

        # Prompt line
        if msg_type == "confirm":
            prompt = "[S]i / [N]o"
        else:
            prompt = "Presione cualquier tecla..."
        px = left + max((box_w - len(prompt)) // 2, 2)
        self._safe_addstr(top + box_h - 2, px, prompt,
                          self._attr("highlight"))
        self.refresh()

        # Wait for key
        if msg_type == "confirm":
            while True:
                key = self.get_key()
                if key in (ord("s"), ord("S")):
                    return True
                if key in (ord("n"), ord("N"), 27):  # 27 = ESC
                    return False
        else:
            self.get_key()
            return True

    def show_input(self, prompt, max_len=40):
        h, w = self.get_size()
        box_w = max(len(prompt) + max_len + 8, 30)
        box_h = 5
        top = max((h - box_h) // 2, 0)
        left = max((w - box_w) // 2, 0)

        buf = []

        try:
            curses.curs_set(1)  # show cursor during input
        except curses.error:
            pass

        while True:
            self.draw_box(top, left, box_h, box_w)
            # Fill interior
            interior_attr = self._attr("normal")
            for r in range(1, box_h - 1):
                self._safe_addstr(top + r, left + 1,
                                  " " * (box_w - 2), interior_attr)

            # Prompt
            self._safe_addstr(top + 1, left + 2, prompt,
                              self._attr("field_label"))

            # Input field
            val = "".join(buf)
            field_text = "[" + pad_right(val, max_len) + "]"
            self._safe_addstr(top + 2, left + 2, field_text,
                              self._attr("field_active"))

            # Hint
            hint = "ENTER=Aceptar  ESC=Cancelar"
            hx = left + max((box_w - len(hint)) // 2, 2)
            self._safe_addstr(top + 3, hx, hint, self._attr("info"))

            self.refresh()

            key = self.get_key()
            if key == 27:  # ESC
                try:
                    curses.curs_set(0)
                except curses.error:
                    pass
                return None
            elif key in (10, curses.KEY_ENTER):  # ENTER
                try:
                    curses.curs_set(0)
                except curses.error:
                    pass
                return "".join(buf)
            elif key in (127, curses.KEY_BACKSPACE, 8):  # BACKSPACE
                if buf:
                    buf.pop()
            elif 32 <= key <= 126:  # printable ASCII
                if len(buf) < max_len:
                    buf.append(chr(key))
