"""File explorer view with Nerd Font icons and tree/flat modes."""

import curses
import os
import shutil
import stat
import time
from pathlib import Path
from datetime import datetime

from muva.views.base import BaseView
from muva import theme

# ── Nerd Font Icons ──────────────────────────────────────────────────────────

ICON_FOLDER_CLOSED = "\uf07b"   #
ICON_FOLDER_OPEN = "\uf07c"     #
ICON_DEFAULT_FILE = "\uf15b"    #
ICON_SYMLINK = "\uf0c1"         #

# Extension -> icon mapping
EXT_ICONS = {
    ".py":          "\ue73c",   #
    ".js":          "\ue74e",   #
    ".jsx":         "\ue74e",   #
    ".ts":          "\ue628",   #
    ".tsx":         "\ue628",   #
    ".json":        "\ue60b",   #
    ".yaml":        "\ue615",   #
    ".yml":         "\ue615",   #
    ".toml":        "\ue615",   #
    ".md":          "\ue73e",   #
    ".markdown":    "\ue73e",   #
    ".rs":          "\ue7a8",   #
    ".go":          "\ue627",   #
    ".html":        "\ue736",   #
    ".htm":         "\ue736",   #
    ".css":         "\ue749",   #
    ".scss":        "\ue749",   #
    ".sh":          "\ue795",   #
    ".bash":        "\ue795",   #
    ".zsh":         "\ue795",   #
    ".fish":        "\ue795",   #
    ".png":         "\uf1c5",   #
    ".jpg":         "\uf1c5",   #
    ".jpeg":        "\uf1c5",   #
    ".gif":         "\uf1c5",   #
    ".svg":         "\uf1c5",   #
    ".ico":         "\uf1c5",   #
    ".lock":        "\uf023",   #
}

# Filename-specific icons
NAME_ICONS = {
    "Dockerfile":       "\uf308",   #
    "docker-compose.yml": "\uf308",
    "docker-compose.yaml": "\uf308",
    ".dockerignore":    "\uf308",
    ".gitignore":       "\ue702",   #
    ".gitmodules":      "\ue702",
    ".gitattributes":   "\ue702",
    ".editorconfig":    "\ue615",   #
    ".env":             "\ue615",
    ".env.local":       "\ue615",
    ".prettierrc":      "\ue615",
    ".eslintrc":        "\ue615",
    "Makefile":         "\ue615",
    "CMakeLists.txt":   "\ue615",
}


def get_icon(entry):
    """Return the Nerd Font icon for a file entry."""
    name = entry.name
    if entry.is_symlink():
        return ICON_SYMLINK
    if entry.is_dir():
        return ICON_FOLDER_CLOSED
    if name in NAME_ICONS:
        return NAME_ICONS[name]
    ext = Path(name).suffix.lower()
    return EXT_ICONS.get(ext, ICON_DEFAULT_FILE)


def get_dir_icon(expanded):
    return ICON_FOLDER_OPEN if expanded else ICON_FOLDER_CLOSED


class ExplorerView(BaseView):
    """VS Code-style file explorer with tree and flat modes."""

    def __init__(self, start_path=None):
        self.path = Path(start_path) if start_path else Path.cwd()
        self.cursor = 0
        self.scroll = 0
        self.selected = set()
        self.clipboard = []
        self.clip_op = None
        self.message = ""
        self.msg_time = 0
        self.search_mode = False
        self.search_query = ""
        self.rename_mode = False
        self.rename_buf = ""
        self.rename_idx = -1
        self.root_locked = False
        self.root_path = Path(self.path)

        # Tree mode state
        self.tree_mode = True
        self.expanded = set()  # set of resolved path strings for expanded dirs
        self._flat_entries = []  # flat list: list of (entry, depth)
        self._raw_entries = []   # raw entries for flat mode

        self.refresh_entries()

    @property
    def title(self):
        return f"\uf07b {self.path.name or '/'}"

    @property
    def current_dir(self):
        return self.path

    def refresh_entries(self):
        """Rebuild entry list based on current mode."""
        if self.tree_mode:
            self._rebuild_tree()
        else:
            self._rebuild_flat()
        # Clamp cursor
        total = len(self._flat_entries)
        if self.cursor >= total:
            self.cursor = max(0, total - 1)

    def _rebuild_flat(self):
        """Flat mode: just list current directory."""
        try:
            items = list(self.path.iterdir())
        except PermissionError:
            self.show_msg("Permission denied")
            self._flat_entries = []
            return

        dirs = sorted([e for e in items if e.is_dir()], key=lambda x: x.name.lower())
        files = sorted([e for e in items if not e.is_dir()], key=lambda x: x.name.lower())
        self._raw_entries = dirs + files
        self._flat_entries = [(e, 0) for e in self._raw_entries]
        # Clean selections
        valid = set(e.name for e in self._raw_entries)
        self.selected = {s for s in self.selected if s in valid}

    def _rebuild_tree(self):
        """Tree mode: build visible tree from expanded state."""
        self._flat_entries = []
        self._build_tree_recursive(self.path, 0)
        valid = set(str(e.resolve()) for e, _ in self._flat_entries)
        self.selected = {s for s in self.selected if s in valid}

    def _build_tree_recursive(self, dir_path, depth):
        try:
            items = list(dir_path.iterdir())
        except PermissionError:
            return
        dirs = sorted([e for e in items if e.is_dir()], key=lambda x: x.name.lower())
        files = sorted([e for e in items if not e.is_dir()], key=lambda x: x.name.lower())
        for entry in dirs + files:
            self._flat_entries.append((entry, depth))
            if entry.is_dir() and str(entry.resolve()) in self.expanded:
                self._build_tree_recursive(entry, depth + 1)

    def entry_key(self, entry):
        """Key for identifying entries in selections."""
        if self.tree_mode:
            return str(entry.resolve())
        return entry.name

    def show_msg(self, msg):
        self.message = msg
        self.msg_time = time.time()

    def fmt_size(self, size):
        for unit in ("B", "K", "M", "G", "T"):
            if size < 1024:
                return f"{size:>5.0f}{unit}" if unit == "B" else f"{size:>5.1f}{unit}"
            size /= 1024
        return f"{size:.1f}P"

    def draw(self, stdscr, h, w, y_offset):
        content_h = h - 3  # status(1) + help(1) + header(1)

        # Header
        header = f" {self.path} "
        lock_icon = "\uf023 " if self.root_locked else ""
        try:
            stdscr.addnstr(y_offset, 0, " " * w, w, theme.cp("explorer_header") | curses.A_DIM)
            stdscr.addnstr(y_offset, 0, header, w, theme.cp("explorer_header") | curses.A_BOLD)
            info = f" {lock_icon}  {len(self._flat_entries)} items "
            if len(info) < w:
                stdscr.addnstr(y_offset, w - len(info) - 1, info, len(info), theme.cp("explorer_header"))
        except curses.error:
            pass

        # Scroll handling
        if self.cursor < self.scroll:
            self.scroll = self.cursor
        if self.cursor >= self.scroll + content_h:
            self.scroll = self.cursor - content_h + 1

        # Entries
        for i in range(content_h):
            idx = self.scroll + i
            row = y_offset + 1 + i
            if idx >= len(self._flat_entries):
                break

            entry, depth = self._flat_entries[idx]
            name = entry.name
            is_dir = entry.is_dir()
            ekey = self.entry_key(entry)
            is_sel = ekey in self.selected
            is_cur = idx == self.cursor
            is_link = entry.is_symlink()

            # Icon
            if is_dir:
                expanded = str(entry.resolve()) in self.expanded
                icon = get_dir_icon(expanded)
            else:
                icon = get_icon(entry)

            # Indentation for tree mode
            indent = "  " * depth if self.tree_mode else ""

            # Color
            if is_cur and is_sel:
                attr = theme.cp("explorer_cur_sel") | curses.A_BOLD
            elif is_cur:
                attr = theme.cp("explorer_cursor") | curses.A_BOLD
            elif is_sel:
                attr = theme.cp("explorer_sel") | curses.A_BOLD
            elif is_link:
                attr = theme.cp("explorer_link")
            elif is_dir:
                attr = theme.cp("explorer_dir") | curses.A_BOLD
            elif os.access(entry, os.X_OK) and entry.is_file():
                attr = theme.cp("explorer_exec")
            else:
                attr = theme.cp("default")

            marker = "\u25c6" if is_sel else " "  # ◆

            # Rename mode
            if self.rename_mode and idx == self.rename_idx:
                display_name = self.rename_buf
                attr = curses.A_UNDERLINE | curses.A_BOLD | theme.cp("fg")
            else:
                display_name = name + ("/" if is_dir else "")

            line = f" {marker} {indent}{icon}  {display_name}"
            try:
                stdscr.addnstr(row, 0, " " * w, w, theme.cp("default"))
                stdscr.addnstr(row, 0, line[:w], w, attr)
            except curses.error:
                pass

        # Status bar
        status_row = y_offset + h - 2
        lock_icon = "\uf023" if self.root_locked else "\uf09c"
        mode_str = " TREE " if self.tree_mode else " FLAT "
        if self.search_mode:
            status = f" / {self.search_query}\u2588"
        elif time.time() - self.msg_time < 3 and self.message:
            status = f" {self.message}"
        elif self.clipboard:
            status = f" Clipboard: {len(self.clipboard)} items ({self.clip_op})"
        else:
            status = f" {lock_icon}  {self.path}"
        right = f" {mode_str} {self.cursor + 1}/{len(self._flat_entries)} "
        gap = w - len(status) - len(right)
        if gap < 0:
            gap = 0
        bar = status + " " * gap + right
        try:
            stdscr.addnstr(status_row, 0, bar[:w], w, theme.cp("statusbar") | curses.A_BOLD)
        except curses.error:
            pass

        # Help bar
        help_text = " j/k:nav  l/Enter:open  h:parent  s:select  Tab:tree/flat  /:search  q:quit"
        try:
            stdscr.addnstr(y_offset + h - 1, 0, help_text[:w-1], w-1, theme.cp("comment"))
        except curses.error:
            pass

    def handle_key(self, key):
        if self.rename_mode:
            return self._handle_rename_key(key)

        if self.search_mode:
            return self._handle_search_key(key)

        if key == ord("q"):
            return "quit"
        elif key == curses.KEY_UP or key == ord("k"):
            self.cursor = max(0, self.cursor - 1)
        elif key == curses.KEY_DOWN or key == ord("j"):
            self.cursor = min(len(self._flat_entries) - 1, self.cursor + 1)
        elif key == curses.KEY_LEFT or key == ord("h"):
            return self._handle_left()
        elif key in (curses.KEY_RIGHT, curses.KEY_ENTER, 10, 13, ord("l")):
            return self._handle_enter()
        elif key == ord("s"):
            self._toggle_select()
        elif key == ord("a"):
            self._select_all()
        elif key == ord("c"):
            self._copy_selected()
        elif key == ord("x"):
            self._cut_selected()
        elif key == ord("v"):
            self._paste()
        elif key == ord("d"):
            self._delete_selected()
        elif key == ord("r"):
            self._start_rename()
        elif key == ord("/"):
            self.search_mode = True
            self.search_query = ""
        elif key == ord("g"):
            self.cursor = 0
        elif key == ord("G"):
            self.cursor = max(0, len(self._flat_entries) - 1)
        elif key == ord("{") or key == curses.KEY_PPAGE:
            self.cursor = max(0, self.cursor - 15)
        elif key == ord("}") or key == curses.KEY_NPAGE:
            self.cursor = min(len(self._flat_entries) - 1, self.cursor + 15)
        elif key == 9:  # Tab
            self.tree_mode = not self.tree_mode
            self.cursor = 0
            self.scroll = 0
            self.selected.clear()
            self.refresh_entries()
        return None

    def _handle_left(self):
        """h/Left: in tree mode, collapse dir or jump to parent entry. In flat, go parent."""
        if not self._flat_entries:
            return None
        if self.tree_mode:
            entry, depth = self._flat_entries[self.cursor]
            if entry.is_dir() and str(entry.resolve()) in self.expanded:
                # Collapse this dir
                self.expanded.discard(str(entry.resolve()))
                self.refresh_entries()
                return None
            # Jump to parent entry in the tree
            if depth > 0:
                for i in range(self.cursor - 1, -1, -1):
                    e, d = self._flat_entries[i]
                    if d < depth and e.is_dir():
                        self.cursor = i
                        return None
            # At root level, go to actual parent dir
            return self._go_parent()
        else:
            return self._go_parent()

    def _go_parent(self):
        if self.root_locked and self.path == self.root_path:
            self.show_msg("Root locked (Leader+L to unlock)")
            return None
        old = self.path.name
        parent = self.path.parent
        if parent == self.path:
            return None
        self.path = parent
        self.cursor = 0
        self.scroll = 0
        self.selected.clear()
        self.expanded.clear()
        self.refresh_entries()
        for i, (e, _) in enumerate(self._flat_entries):
            if e.name == old:
                self.cursor = i
                break
        return None

    def _handle_enter(self):
        """Enter/Right/l: open dir or file.

        Unlocked: navigate into dirs (change root).
        Locked: expand/collapse dirs as subtrees.
        """
        if not self._flat_entries:
            return None
        entry, depth = self._flat_entries[self.cursor]
        if entry.is_dir():
            if self.root_locked:
                # Tree expand/collapse
                resolved = str(entry.resolve())
                if resolved in self.expanded:
                    self.expanded.discard(resolved)
                else:
                    try:
                        list(entry.iterdir())
                        self.expanded.add(resolved)
                    except PermissionError:
                        self.show_msg("Permission denied")
                        return None
                self.refresh_entries()
            else:
                # Navigate into the directory
                try:
                    list(entry.iterdir())
                except PermissionError:
                    self.show_msg("Permission denied")
                    return None
                self.path = entry.resolve()
                self.cursor = 0
                self.scroll = 0
                self.selected.clear()
                self.expanded.clear()
                self.refresh_entries()
        elif entry.is_file():
            return f"open:{entry.resolve()}"
        return None

    def _toggle_select(self):
        if not self._flat_entries:
            return
        entry, _ = self._flat_entries[self.cursor]
        ekey = self.entry_key(entry)
        if ekey in self.selected:
            self.selected.discard(ekey)
        else:
            self.selected.add(ekey)
        self.cursor = min(self.cursor + 1, len(self._flat_entries) - 1)

    def _select_all(self):
        all_keys = {self.entry_key(e) for e, _ in self._flat_entries}
        if self.selected == all_keys:
            self.selected.clear()
        else:
            self.selected = all_keys

    def _get_targets(self):
        """Return list of Path objects for selected or cursor item."""
        if self.selected:
            result = []
            for entry, _ in self._flat_entries:
                if self.entry_key(entry) in self.selected:
                    result.append(entry)
            return result
        if self._flat_entries:
            return [self._flat_entries[self.cursor][0]]
        return []

    def _copy_selected(self):
        targets = self._get_targets()
        if not targets:
            return
        self.clipboard = [Path(t) for t in targets]
        self.clip_op = "copy"
        self.show_msg(f"Copied {len(self.clipboard)} items")

    def _cut_selected(self):
        targets = self._get_targets()
        if not targets:
            return
        self.clipboard = [Path(t) for t in targets]
        self.clip_op = "cut"
        self.show_msg(f"Cut {len(self.clipboard)} items")

    def _paste(self):
        if not self.clipboard:
            self.show_msg("Clipboard empty")
            return
        count = 0
        for src in self.clipboard:
            dst = self.path / src.name
            if dst == src:
                base = src.stem + "_copy" + src.suffix
                dst = self.path / base
            try:
                if src.is_dir():
                    if self.clip_op == "copy":
                        shutil.copytree(src, dst)
                    else:
                        shutil.move(str(src), str(dst))
                else:
                    if self.clip_op == "copy":
                        shutil.copy2(src, dst)
                    else:
                        shutil.move(str(src), str(dst))
                count += 1
            except Exception as e:
                self.show_msg(f"Error: {e}")
                return
        if self.clip_op == "cut":
            self.clipboard.clear()
        self.show_msg(f"Pasted {count} items")
        self.selected.clear()
        self.refresh_entries()

    def _delete_selected(self):
        targets = self._get_targets()
        if not targets:
            return
        self.show_msg(f"Delete {len(targets)} items? y/n")
        # The app will need to handle the confirmation - store pending state
        self._pending_delete = targets

    def confirm_delete(self, stdscr):
        """Called by app after showing delete prompt."""
        targets = getattr(self, "_pending_delete", None)
        if not targets:
            return
        k = stdscr.getch()
        if k != ord("y"):
            self.show_msg("Cancelled")
            self._pending_delete = None
            return
        for entry in targets:
            try:
                if entry.is_dir():
                    shutil.rmtree(entry)
                else:
                    entry.unlink()
            except Exception as e:
                self.show_msg(f"Error: {e}")
                self._pending_delete = None
                return
        self.selected.clear()
        self.refresh_entries()
        self.show_msg(f"Deleted {len(targets)} items")
        self._pending_delete = None

    def _start_rename(self):
        if not self._flat_entries:
            return
        self.rename_mode = True
        self.rename_idx = self.cursor
        self.rename_buf = self._flat_entries[self.cursor][0].name

    def _handle_rename_key(self, key):
        if key == 27:
            self.rename_mode = False
            return None
        if key in (curses.KEY_ENTER, 10, 13):
            entry, _ = self._flat_entries[self.rename_idx]
            new = entry.parent / self.rename_buf
            if self.rename_buf and new != entry:
                try:
                    entry.rename(new)
                    self.show_msg(f"Renamed to {self.rename_buf}")
                except Exception as e:
                    self.show_msg(f"Error: {e}")
            self.rename_mode = False
            self.refresh_entries()
            return None
        if key in (curses.KEY_BACKSPACE, 127, 8):
            self.rename_buf = self.rename_buf[:-1]
            return None
        if 32 <= key <= 126:
            self.rename_buf += chr(key)
        return None

    def _handle_search_key(self, key):
        if key == 27:
            self.search_mode = False
            self.search_query = ""
        elif key in (curses.KEY_ENTER, 10, 13):
            self.search_mode = False
            self._do_search()
            self.search_query = ""
        elif key in (curses.KEY_BACKSPACE, 127, 8):
            self.search_query = self.search_query[:-1]
        elif 32 <= key <= 126:
            self.search_query += chr(key)
        return None

    def _do_search(self):
        if not self.search_query:
            return
        q = self.search_query.lower()
        for i, (e, _) in enumerate(self._flat_entries):
            if q in e.name.lower():
                self.cursor = i
                self.show_msg(f"Found: {e.name}")
                return
        self.show_msg("Not found")

    def on_enter(self):
        self.refresh_entries()
