from __future__ import annotations

import json
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Optional

try:
    from colorama import init as colorama_init
except Exception:  # pragma: no cover
    def colorama_init(*args, **kwargs):
        return None

from .assistant import AssistantManager
from .clipboard import ClipboardHelper
from .config import ConfigManager, WorkspaceProfileManager
from .events import EventBus
from .icons import IconPack, get_icon_pack
from .input_driver import disable_mouse, enable_mouse, read_command
from .ops import BackgroundJobManager, DependencyGraph, HealthSuite, PortChecker, ProcessMonitor, RecentProjects, Scheduler, ServiceController, TaskRunner
from .plugins import Plugin, PluginManager, WidgetRegistry
from .render import animate_once, box, chip, clear_screen, color_line, divider, footer_bar, highlight, panel, safe_width, section_header, stat_row, tab_strip, term_size, widget_card, wrap_block
from .theme import THEMES, Theme, ThemeEditor, get_theme
from .voice import VoiceEngine, VoiceRouter
from .widgets import (
    AssistantPanel,
    Badge,
    Button,
    Clock,
    CommandPalette,
    ContextMenu,
    CustomWidget,
    Divider,
    FilePicker,
    Form,
    Input,
    JSONViewer,
    KeyValue,
    ListWidget,
    LogViewer,
    Logger,
    MultiSelect,
    Paragraph,
    Progress,
    Section,
    Select,
    SettingPage,
    StatCard,
    StatusTable,
    SubTabView,
    Table,
    TerminalOutputPanel,
    Text,
    Toggle,
    TranscriptPanel,
    TreeNode,
    TreeWidget,
    VoiceCommand,
    Widget,
)

colorama_init(autoreset=True)


@dataclass
class WindowConfig:
    title: str
    subtitle: str = ''
    theme: str = 'dark'
    icon_pack: str = 'emoji'
    density: str = 'cozy'
    accessibility_mode: bool = False
    mouse_support: bool = False


class ShortcutManager:
    def __init__(self) -> None:
        self.shortcuts: dict[str, tuple[str, Callable[[], None]]] = {}

    def register(self, key: str, description: str, callback: Callable[[], None]) -> None:
        self.shortcuts[key.lower()] = (description, callback)

    def trigger(self, key: str) -> bool:
        item = self.shortcuts.get(key.lower())
        if not item:
            return False
        item[1]()
        return True

    def rows(self) -> list[list[str]]:
        return [[key, desc] for key, (desc, _) in sorted(self.shortcuts.items())]


class Tab:
    def __init__(self, title: str):
        self.title = title
        self.sections: list[Section] = []

    def create_section(self, title: str) -> Section:
        section = Section(title)
        self.sections.append(section)
        return section


class Window:
    def __init__(self, config: WindowConfig):
        self.config = config
        self.theme: Theme = get_theme(config.theme)
        self.theme_editor = ThemeEditor(self.theme)
        self.icon_pack: IconPack = get_icon_pack(config.icon_pack)
        self.tabs: list[Tab] = []
        self._active_tab_index = 0
        self._focus_index = 0
        self._is_running = False
        self._last_size = term_size()
        self._on_open: list[Callable[['Window'], None]] = []
        self._on_close: list[Callable[['Window'], None]] = []
        self._notifications: list[tuple[str, str]] = []
        self._global_commands: list[tuple[str, str, Callable[[], None]]] = []
        self.event_bus = EventBus()
        self.shortcut_manager = ShortcutManager()
        self.clipboard = ClipboardHelper()
        self.config_manager = ConfigManager(Path('.nest_ui/config.json'), defaults={'theme': config.theme, 'density': config.density, 'icon_pack': config.icon_pack})
        self.workspace_profiles = WorkspaceProfileManager(Path('.nest_ui/workspace_profiles.json'))
        self.voice_engine = VoiceEngine()
        self.voice_router = VoiceRouter(wake_word='nest', cooldown_seconds=0.0, language_pack='en')
        self.assistant = AssistantManager(name='Nest', voice_engine=self.voice_engine, wake_word='nest', enabled=True, listen_enabled=True, voice_reply_enabled=False).bind_window(self)
        self._mouse_regions: list[tuple[int, int, int]] = []
        self.plugin_manager = PluginManager()
        self.widget_registry = WidgetRegistry()
        self.task_runner = TaskRunner()
        self.background_jobs = BackgroundJobManager()
        self.service_controller = ServiceController()
        self.process_monitor = ProcessMonitor()
        self.port_checker = PortChecker()
        self.health_suite = HealthSuite()
        self.dependency_graph = DependencyGraph()
        self.scheduler = Scheduler()
        self.recent_projects = RecentProjects(Path('.nest_ui/recent_projects.json'))
        self.create_tab('Main')
        self._register_defaults()
        if self.config.mouse_support:
            enable_mouse()

    def _register_defaults(self) -> None:
        self.register_command('help', 'Show help', self.help)
        self.register_command('tabs', 'Switch tabs', self._change_tab)
        self.register_command('palette', 'Open command palette', self.open_command_palette)
        self.register_command('focus-next', 'Move focus down', lambda: self._move_focus(1))
        self.register_command('focus-prev', 'Move focus up', lambda: self._move_focus(-1))
        self.register_command('activate', 'Activate focused widget', self._activate_focused)
        self.register_command('voice', 'Voice / typed command', self.open_voice_prompt)
        self.register_command('settings', 'Open settings page', self.open_settings_overlay)
        self.register_command('assistant', 'Open assistant prompt', self.open_assistant_prompt)
        self.register_command('onboarding', 'Open onboarding overlay', self.open_onboarding)
        self.shortcut_manager.register('/', 'Open command palette', self.open_command_palette)
        self.shortcut_manager.register('?', 'Open help', self.help)
        self.shortcut_manager.register('v', 'Open voice prompt', self.open_voice_prompt)
        self.shortcut_manager.register('A', 'Open assistant prompt', self.open_assistant_prompt)

    @property
    def current_tab(self) -> Tab:
        return self.tabs[self._active_tab_index]

    def icon(self, key: str, fallback: str = '') -> str:
        return self.icon_pack.get(key, fallback)

    def create_tab(self, title: str) -> Tab:
        tab = Tab(title)
        self.tabs.append(tab)
        return tab

    def create_section(self, title: str) -> Section:
        return self.current_tab.create_section(title)

    def on_open(self, callback: Callable[['Window'], None]) -> 'Window':
        self._on_open.append(callback)
        return self

    def on_close(self, callback: Callable[['Window'], None]) -> 'Window':
        self._on_close.append(callback)
        return self

    def register_command(self, name: str, description: str, callback: Callable[[], None]) -> None:
        self._global_commands.append((name, description, callback))

    def register_shortcut(self, key: str, description: str, callback: Callable[[], None]) -> None:
        self.shortcut_manager.register(key, description, callback)

    def register_widget_type(self, name: str, render: Callable[['Window', CustomWidget, int], str], handle: Callable[['Window', CustomWidget], None] | None = None) -> None:
        self.widget_registry.register(name, render, handle)

    def create_assistant(self, name: str = 'Nest', *, enabled: bool = True, listen_enabled: bool = True, voice_reply_enabled: bool = False, wake_word: str = 'nest', persona: str = 'assistant', voice_profile: str = 'nova') -> AssistantManager:
        self.assistant.name = name
        self.assistant.enabled = enabled
        self.assistant.listen_enabled = listen_enabled
        self.assistant.voice_reply_enabled = voice_reply_enabled
        self.assistant.wake_word = wake_word
        self.assistant.persona = persona
        self.assistant.voice_profile = voice_profile
        return self.assistant

    def enable_assistant(self, enabled: bool = True, **kwargs: Any) -> AssistantManager:
        assistant = self.create_assistant(enabled=enabled, **kwargs)
        self.notify(f'Assistant: {"on" if enabled else "off"}', tone='success' if enabled else 'warning')
        return assistant

    def use_plugin(self, plugin: Plugin) -> None:
        if not self.plugin_manager.register(plugin):
            self.notify(f'Plugin blocked by sandbox: {getattr(plugin, "name", plugin.__class__.__name__)}', tone='danger')
            return
        plugin.setup(self)
        meta = self.plugin_manager.metadata[getattr(plugin, 'metadata').name if hasattr(plugin, 'metadata') else getattr(plugin, 'name', plugin.__class__.__name__)]
        self.notify(f'Plugin loaded: {meta.name}', tone='success')
        self.log('INFO', f'plugin loaded -> {meta.name}')
        self.event_bus.emit('plugin.loaded', meta)

    def set_theme(self, theme_name: str) -> None:
        self.theme = get_theme(theme_name)
        self.theme_editor.theme = self.theme
        self.config.theme = self.theme.name
        self.config_manager.set('theme', self.theme.name)
        self.notify(f'Theme changed to {self.theme.name}', tone='success')

    def set_icon_pack(self, name: str) -> None:
        self.icon_pack = get_icon_pack(name)
        self.config.icon_pack = self.icon_pack.name
        self.config_manager.set('icon_pack', self.icon_pack.name)
        self.notify(f'Icon pack changed to {self.icon_pack.name}', tone='success')

    def set_density(self, mode: str) -> None:
        mode = mode.lower().strip()
        if mode not in {'compact', 'cozy'}:
            mode = 'cozy'
        self.config.density = mode
        self.config_manager.set('density', mode)
        self.notify(f'Density mode: {mode}', tone='info')

    def set_accessibility_mode(self, enabled: bool) -> None:
        self.config.accessibility_mode = bool(enabled)
        self.notify(f'Accessibility mode: {"on" if enabled else "off"}', tone='info')

    def set_mouse_support(self, enabled: bool) -> None:
        self.config.mouse_support = bool(enabled)
        if enabled:
            enable_mouse()
        else:
            disable_mouse()
        self.notify(f'Mouse support: {"on" if enabled else "off"}', tone='info')

    def notify(self, message: str, tone: str = 'info') -> None:
        self._notifications.append((tone, message))
        self._notifications = self._notifications[-6:]
        self.event_bus.emit('notify', tone, message)

    def log(self, *args) -> None:
        if len(args) == 1:
            level, message = 'INFO', str(args[0])
        elif len(args) >= 2:
            level, message = str(args[0]).upper(), str(args[1])
        else:
            level, message = 'INFO', ''
        for widget in self._all_widgets():
            if isinstance(widget, (Logger, LogViewer)):
                widget.log(level, message)
        self.event_bus.emit('log', level, message)

    def modal(self, title: str, body: str, footer: str = 'Press Enter to close') -> None:
        clear_screen()
        lines = wrap_block(body)
        lines.append('')
        lines.append(color_line(footer, self.theme.muted))
        print(panel(f'{self.icon("info")} {title}', lines, self.theme))
        read_command(color_line('Enter...', self.theme.muted))

    def confirm(self, title: str, body: str, *, yes_text: str = 'y', no_text: str = 'n', default: bool = False) -> bool:
        clear_screen()
        print(panel(f'{self.icon("warning")} {title}', wrap_block(body), self.theme))
        raw = read_command(color_line(f'Confirm [{yes_text.upper()}/{no_text.upper()}]: ', self.theme.text)).strip().lower()
        if not raw:
            return default
        return raw == yes_text.lower()

    def help(self) -> None:
        lines = [
            'number -> activate widget',
            '↑/↓ or :up/:down -> move focus',
            '←/→ -> change tabs',
            'enter or a -> activate focused widget',
            't -> switch tabs',
            '/ -> command palette',
            'voice -> voice bridge',
            'assistant -> assistant bridge',
            'theme <name> -> change theme',
            'icons <emoji|ascii> -> change icon pack',
            'density <compact|cozy> -> change spacing',
            'settings -> quick settings',
            'q / quit / exit -> close window',
            'themes -> show all bundled themes',
        ]
        self.modal('Help', '\n'.join(lines))

    def open_onboarding(self) -> None:
        body = '\n'.join([
            'Welcome to Nest UI Community Framework.',
            '1) Create a window, tabs, and sections.',
            '2) Add widgets like tables, forms, logs, and task panels.',
            '3) Register commands, shortcuts, plugins, and voice phrases.',
            '4) Save state or workspace profiles for real projects.',
        ])
        self.modal('Onboarding', body)

    def open_settings_overlay(self) -> None:
        clear_screen()
        print(panel('Settings', [
            f'theme: {self.theme.name}',
            f'icon pack: {self.icon_pack.name}',
            f'density: {self.config.density}',
            f'accessibility: {self.config.accessibility_mode}',
            f'mouse: {self.config.mouse_support}',
        ], self.theme))
        raw = read_command(color_line('Set (theme/icons/density/accessibility/mouse) or Enter: ', self.theme.text)).strip()
        if not raw:
            return
        if raw.startswith('theme '):
            self.set_theme(raw.split(' ', 1)[1])
        elif raw.startswith('icons '):
            self.set_icon_pack(raw.split(' ', 1)[1])
        elif raw.startswith('density '):
            self.set_density(raw.split(' ', 1)[1])
        elif raw.startswith('accessibility '):
            self.set_accessibility_mode(raw.split(' ', 1)[1].lower() in {'1', 'true', 'on', 'yes'})
        elif raw.startswith('mouse '):
            self.set_mouse_support(raw.split(' ', 1)[1].lower() in {'1', 'true', 'on', 'yes'})

    def open_command_palette(self) -> None:
        commands = list(self._global_commands)
        for widget in self._all_widgets():
            if isinstance(widget, CommandPalette):
                commands.extend([(n, d, cb or (lambda: None)) for n, d, cb in widget.commands])
        clear_screen()
        print(box(f'{self.icon("command_palette")} Command Palette', 'Type number or command name', self.theme))
        for idx, (name, description, _) in enumerate(commands, start=1):
            print(color_line(f'{idx}. {name:<22} {description}', self.theme.text))
        raw = read_command(color_line('Command: ', self.theme.text)).strip()
        if not raw:
            return
        target = None
        if raw.isdigit():
            i = int(raw) - 1
            if 0 <= i < len(commands):
                target = commands[i]
        else:
            for item in commands:
                if item[0].lower() == raw.lower():
                    target = item
                    break
        if not target:
            self._pause('Unknown command.')
            return
        target[2]()

    def open_voice_prompt(self) -> None:
        captured = self.voice_engine.listen_best_effort() if self.voice_engine.available else None
        command = captured.text.strip() if captured else ''
        provider = captured.provider if captured else 'typed'
        if not command:
            if captured and captured.error:
                self.notify(f'Microphone fallback: {captured.error}', tone='warning')
            command = read_command(color_line('Voice command (typed fallback): ', self.theme.text)).strip()
            provider = 'typed'
        if not command:
            self._pause('No command captured.')
            return
        if self.assistant.enabled:
            result = self.assistant.process(command, source=provider, confirm_handler=lambda action: self.confirm('Confirm action', f'Run assistant action: {action}?'))
            if result.get('ok'):
                return
        matched = False
        for widget in self._all_widgets():
            if isinstance(widget, VoiceCommand):
                matched = True
                widget.submit(command, confirm_handler=lambda action: self.confirm('Confirm action', f'Run voice action: {action}?'))
        if not matched:
            result = self.voice_router.execute(command)
            if not result.get('ok'):
                self._pause(f'Captured: {command}')

    def open_assistant_prompt(self) -> None:
        if self.assistant.listen_enabled and self.voice_engine.available:
            captured = self.voice_engine.listen_best_effort()
            text_value = captured.text.strip()
            provider = captured.provider
            if not text_value and captured.error:
                self.notify(f'Assistant microphone fallback: {captured.error}', tone='warning')
        else:
            captured = None
            text_value = ''
            provider = 'typed'
        if not text_value:
            text_value = read_command(color_line(f'{self.assistant.name} command: ', self.theme.text)).strip()
            provider = 'typed'
        if not text_value:
            self._pause('No assistant input captured.')
            return
        self.assistant.process(text_value, source=provider, confirm_handler=lambda action: self.confirm('Assistant confirmation', f'Run assistant action: {action}?'))

    def run(self) -> None:
        self._is_running = True
        for callback in self._on_open:
            callback(self)
        while self._is_running:
            self.scheduler.tick()
            self._detect_resize()
            self._draw()
            raw = read_command(color_line('> ', self.theme.text)).strip()
            if not raw:
                continue
            if raw.lower() in {'q', 'quit', 'exit'}:
                self._is_running = False
                break
            if raw.startswith(':click:'):
                self._handle_mouse_click(raw)
                continue
            if raw in {':up', 'k'}:
                self._move_focus(-1)
                continue
            if raw in {':down', 'j'}:
                self._move_focus(1)
                continue
            if raw == ':left':
                self._step_tab(-1)
                continue
            if raw == ':right':
                self._step_tab(1)
                continue
            if raw in {'a', 'enter'}:
                self._activate_focused()
                continue
            if raw.lower() in {'t', 'tab'}:
                self._change_tab()
                continue
            if self.shortcut_manager.trigger(raw):
                continue
            if raw == 'themes':
                self.modal('Themes', ', '.join(sorted(THEMES.keys())))
                continue
            if raw.startswith('theme '):
                self.set_theme(raw.split(' ', 1)[1])
                continue
            if raw.startswith('icons '):
                self.set_icon_pack(raw.split(' ', 1)[1])
                continue
            if raw.startswith('density '):
                self.set_density(raw.split(' ', 1)[1])
                continue
            if raw.startswith('copy '):
                ok = self.clipboard.copy(raw.split(' ', 1)[1])
                self.notify('Copied to clipboard' if ok else 'Clipboard fallback saved', tone='success' if ok else 'warning')
                continue
            if raw.startswith('/'):
                self._handle_search(raw[1:])
                continue
            if raw == 'x':
                self._clear_searches()
                continue
            if raw.isdigit():
                self._activate_by_number(int(raw))
                continue
            lowered = raw.lower()
            if lowered in {name.lower() for name, _, _ in self._global_commands}:
                for name, _, cb in self._global_commands:
                    if lowered == name.lower():
                        cb()
                        break
                continue
            self.notify(f'Unknown command: {raw}', tone='warning')
        for callback in self._on_close:
            callback(self)
        self.config_manager.save()
        self.workspace_profiles.save()
        self.recent_projects.save()
        if self.config.mouse_support:
            disable_mouse()

    def save_state(self, path: str | Path) -> Path:
        payload = {'theme': self.theme.name, 'icon_pack': self.icon_pack.name, 'density': self.config.density, 'tabs': []}
        for tab in self.tabs:
            tab_data = {'title': tab.title, 'sections': []}
            for section in tab.sections:
                section_data = {'title': section.title, 'widgets': []}
                for widget in section.widgets:
                    section_data['widgets'].append({'kind': widget.kind, 'title': widget.title, 'state': widget.as_state()})
                tab_data['sections'].append(section_data)
            payload['tabs'].append(tab_data)
        path = Path(path)
        path.write_text(json.dumps(payload, indent=2), encoding='utf-8')
        self.notify(f'State saved -> {path.name}', tone='success')
        return path

    def load_state(self, path: str | Path) -> None:
        path = Path(path)
        if not path.exists():
            self.notify(f'Missing state file: {path}', tone='warning')
            return
        payload = json.loads(path.read_text(encoding='utf-8'))
        self.set_theme(payload.get('theme', self.theme.name))
        self.set_icon_pack(payload.get('icon_pack', self.icon_pack.name))
        self.set_density(payload.get('density', self.config.density))
        tab_map = {tab.title: tab for tab in self.tabs}
        for tab_data in payload.get('tabs', []):
            tab = tab_map.get(tab_data.get('title'))
            if not tab:
                continue
            section_map = {section.title: section for section in tab.sections}
            for section_data in tab_data.get('sections', []):
                section = section_map.get(section_data.get('title'))
                if not section:
                    continue
                widget_map = {(widget.kind, widget.title): widget for widget in section.widgets}
                for widget_data in section_data.get('widgets', []):
                    widget = widget_map.get((widget_data.get('kind'), widget_data.get('title')))
                    if not widget:
                        continue
                    state = widget_data.get('state')
                    if isinstance(widget, Toggle):
                        widget.value = bool(state)
                    elif isinstance(widget, Input):
                        widget.value = str(state or '')
                    elif isinstance(widget, Select):
                        if str(state or '') in widget.options:
                            widget.value = str(state)
                    elif isinstance(widget, MultiSelect) and isinstance(state, dict):
                        widget.selected = list(state.get('selected', []))
                        widget.selected_index = int(state.get('selected_index', 0))
                    elif isinstance(widget, Badge):
                        widget.value = str(state or '')
                    elif isinstance(widget, Progress):
                        widget.value = int(state or 0)
                    elif isinstance(widget, (Table, StatusTable)) and isinstance(state, dict):
                        widget.columns = list(state.get('columns', widget.columns))
                        widget.rows = [list(r) for r in state.get('rows', widget.rows)]
                        widget.selected_row = int(state.get('selected_row', 0))
                        widget.search_query = str(state.get('search_query', ''))
                        widget.sort_column = int(state.get('sort_column', 0))
                        widget.sort_reverse = bool(state.get('sort_reverse', False))
                        widget.current_page = int(state.get('current_page', 1))
                        widget.filters = dict(state.get('filters', {}))
                    elif isinstance(widget, (Logger, LogViewer)) and isinstance(state, list):
                        widget.entries = list(state)
                    elif isinstance(widget, TerminalOutputPanel) and isinstance(state, list):
                        widget.lines = list(state)
        self.notify(f'State loaded -> {path.name}', tone='success')

    def save_workspace_profile(self, name: str, payload: dict[str, Any]) -> None:
        self.workspace_profiles.set(name, payload)
        self.workspace_profiles.save()
        self.notify(f'Workspace profile saved: {name}', tone='success')

    def load_workspace_profile(self, name: str) -> dict[str, Any] | None:
        profile = self.workspace_profiles.get(name)
        if profile:
            self.notify(f'Workspace profile loaded: {name}', tone='success')
            return profile.data
        self.notify(f'Workspace profile missing: {name}', tone='warning')
        return None

    def _pause(self, message: str = '') -> None:
        if message:
            print(color_line(message, self.theme.muted))
        read_command(color_line('Enter...', self.theme.muted))

    def _change_tab(self) -> None:
        clear_screen()
        print(box('Tabs', 'Select a tab', self.theme))
        for idx, tab in enumerate(self.tabs, start=1):
            print(color_line(f'{idx}. {tab.title}', self.theme.text))
        raw = read_command(color_line('Tab: ', self.theme.text)).strip()
        if raw.isdigit():
            i = int(raw) - 1
            if 0 <= i < len(self.tabs):
                self._active_tab_index = i
                self._focus_index = 0

    def _step_tab(self, delta: int) -> None:
        if not self.tabs:
            return
        self._active_tab_index = (self._active_tab_index + delta) % len(self.tabs)
        self._focus_index = 0

    def _all_widgets(self) -> list[Widget]:
        result: list[Widget] = []
        for tab in self.tabs:
            for section in tab.sections:
                result.extend(section.widgets)
        return result

    def _indexed_widgets(self) -> list[tuple[Section, Widget]]:
        indexed = []
        for section in self.current_tab.sections:
            for widget in section.widgets:
                indexed.append((section, widget))
        return indexed

    def _move_focus(self, delta: int) -> None:
        widgets = self._indexed_widgets()
        if not widgets:
            self._focus_index = 0
            return
        self._focus_index = max(0, min(self._focus_index + delta, len(widgets) - 1))

    def _activate_by_number(self, number: int) -> None:
        widgets = self._indexed_widgets()
        i = number - 1
        if 0 <= i < len(widgets):
            self._focus_index = i
            self._activate_focused()

    def _activate_focused(self) -> None:
        widgets = self._indexed_widgets()
        if not widgets:
            return
        _, widget = widgets[self._focus_index]
        self._handle_widget(widget)

    def _handle_mouse_click(self, raw: str) -> None:
        try:
            _, _, x_str, y_str, *_rest = raw.split(':')
            y = int(y_str)
        except Exception:
            return
        for start, end, index in self._mouse_regions:
            if start <= y <= end:
                self._focus_index = max(0, index - 1)
                self._activate_focused()
                return

    def _handle_search(self, query: str) -> None:
        widgets = self._indexed_widgets()
        if not widgets:
            return
        _, widget = widgets[self._focus_index]
        if isinstance(widget, (Table, StatusTable)):
            widget.set_search_query(query)
            self.notify(f'Table search: {query}', tone='info')
        elif isinstance(widget, LogViewer):
            widget.search_query = query
            self.notify(f'Log search: {query}', tone='info')

    def _clear_searches(self) -> None:
        for _, widget in self._indexed_widgets():
            if isinstance(widget, (Table, StatusTable)):
                widget.clear_search()
            elif isinstance(widget, LogViewer):
                widget.search_query = ''
        self.notify('Cleared search', tone='info')

    def _handle_widget(self, widget: Widget) -> None:
        if widget.disabled:
            self.notify(f'Widget disabled: {widget.title}', tone='warning')
            return
        if isinstance(widget, Button):
            if widget.dangerous and not self.confirm('Dangerous action', f'Run "{widget.title}"?'):
                return
            if widget.callback:
                widget.callback()
            if widget.close_on_press:
                self._is_running = False
            return
        if isinstance(widget, Toggle):
            widget.flip(); return
        if isinstance(widget, Input):
            raw = read_command(color_line(f'{widget.title}: ', self.theme.text))
            widget.set_value(raw)
            widget.validate()
            return
        if isinstance(widget, Select):
            self._select_from_options(widget.title, widget.options, widget.set_value); widget.validate(); return
        if isinstance(widget, MultiSelect):
            self._multiselect_dialog(widget); return
        if isinstance(widget, (Table, StatusTable)):
            self._table_dialog(widget); return
        if isinstance(widget, (Logger, LogViewer)):
            self._logger_dialog(widget); return
        if isinstance(widget, TerminalOutputPanel):
            self._terminal_dialog(widget); return
        if isinstance(widget, ListWidget):
            widget.activate(); return
        if isinstance(widget, TreeWidget):
            widget.toggle_selected(); widget.activate(); return
        if isinstance(widget, Form):
            ok, errors = widget.submit(); self.modal('Form Result', 'Submitted successfully.' if ok else '\n'.join(errors)); return
        if isinstance(widget, SettingPage):
            payload = widget.save(); self.modal('Settings Saved', json.dumps(payload, indent=2)); return
        if isinstance(widget, ContextMenu):
            self._context_menu_dialog(widget); return
        if isinstance(widget, FilePicker):
            choices = widget.choices(); self._select_from_options(widget.title, choices, widget.select); return
        if isinstance(widget, JSONViewer):
            self.modal(widget.title, widget.text()); return
        if isinstance(widget, TranscriptPanel):
            body = '\n'.join(widget.preview_lines())
            self.modal(widget.title, body); return
        if isinstance(widget, AssistantPanel):
            manager = widget.manager or self.assistant
            captured = manager.capture(widget.timeout, widget.phrase_time_limit) if widget.prefer_microphone and manager.listen_enabled and self.voice_engine.available else {'text': '', 'provider': 'typed', 'error': ''}
            raw = str(captured.get('text') or '').strip()
            provider = str(captured.get('provider') or 'typed')
            if not raw:
                if captured.get('error'):
                    self.notify(f'Assistant fallback: {captured.get("error")}', tone='warning')
                raw = read_command(color_line(f'{manager.name}: ', self.theme.text)).strip()
                provider = 'typed'
            if raw:
                manager.process(raw, source=provider, confirm_handler=lambda action: self.confirm('Assistant confirmation', f'Run assistant action "{action}"?'))
            return
        if isinstance(widget, VoiceCommand):
            captured = self.voice_engine.listen_best_effort(widget.timeout, widget.phrase_time_limit) if widget.prefer_microphone and self.voice_engine.available else None
            raw = captured.text if captured else ''
            if not raw:
                raw = read_command(color_line(f'{widget.placeholder}: ', self.theme.text))
            if raw:
                widget.submit(raw, confirm_handler=lambda action: self.confirm('Voice confirmation', f'Run voice action "{action}"?'))
            return
        if isinstance(widget, CustomWidget):
            spec = self.widget_registry.specs.get(widget.custom_type)
            if spec and spec.handle:
                spec.handle(self, widget)
            elif widget.callback:
                widget.callback(widget)
            return

    def _select_from_options(self, title: str, options: list[str], setter: Callable[[str], None]) -> None:
        clear_screen()
        print(box(title, 'Choose a value', self.theme))
        for idx, value in enumerate(options, start=1):
            print(color_line(f'{idx}. {value}', self.theme.text))
        raw = read_command(color_line('Choice: ', self.theme.text)).strip()
        if raw.isdigit():
            i = int(raw) - 1
            if 0 <= i < len(options):
                setter(options[i])

    def _multiselect_dialog(self, widget: MultiSelect) -> None:
        clear_screen()
        print(box(widget.title, 'Toggle items by number, comma-separated', self.theme))
        for idx, value in enumerate(widget.options, start=1):
            marker = '[x]' if value in widget.selected else '[ ]'
            print(color_line(f'{idx}. {marker} {value}', self.theme.text))
        raw = read_command(color_line('Toggle: ', self.theme.text)).strip()
        for part in [p.strip() for p in raw.split(',') if p.strip()]:
            if part.isdigit():
                i = int(part) - 1
                if 0 <= i < len(widget.options):
                    widget.toggle_option(widget.options[i])

    def _table_dialog(self, widget: Table) -> None:
        clear_screen()
        lines = [f'Columns: {", ".join(widget.columns)}', f'Rows visible: {len(widget.visible_rows())}', f'Page {widget.current_page}/{widget.max_pages()}']
        for idx, (_, row) in enumerate(widget.page_rows(), start=1):
            prefix = '>' if idx - 1 == widget.selected_row else ' '
            lines.append(f'{prefix} {idx}. ' + ' | '.join(row))
        lines.append('')
        lines.append('Commands: search <q>, sort <column>, next, prev, edit row,col,value, del')
        print(panel(widget.title, lines, self.theme))
        raw = read_command(color_line('Table: ', self.theme.text)).strip()
        if raw.startswith('search '):
            widget.set_search_query(raw.split(' ', 1)[1])
        elif raw.startswith('sort '):
            widget.sort_by(raw.split(' ', 1)[1])
        elif raw == 'next':
            widget.next_page()
        elif raw == 'prev':
            widget.prev_page()
        elif raw == 'del':
            widget.delete_selected_row()
        elif raw.startswith('edit '):
            payload = raw.split(' ', 1)[1]
            parts = [p.strip() for p in payload.split(',')]
            if len(parts) >= 3 and parts[0].isdigit():
                row_index = int(parts[0]) - 1
                widget.edit_cell(row_index, parts[1], ','.join(parts[2:]))

    def _logger_dialog(self, widget: Logger | LogViewer) -> None:
        clear_screen()
        if isinstance(widget, LogViewer):
            entries = widget.visible_entries()
        else:
            entries = widget.entries
        body = [f'[{entry["level"]}] {entry["message"]}' for entry in entries[-20:]] or ['(no logs)']
        body.append('')
        body.append('Commands: clear, filter <LEVEL>, search <text>')
        print(panel(widget.title, body, self.theme))
        raw = read_command(color_line('Logs: ', self.theme.text)).strip()
        if raw == 'clear':
            widget.clear()
        elif isinstance(widget, LogViewer) and raw.startswith('filter '):
            widget.level_filter = raw.split(' ', 1)[1].upper()
        elif isinstance(widget, LogViewer) and raw.startswith('search '):
            widget.search_query = raw.split(' ', 1)[1]

    def _terminal_dialog(self, widget: TerminalOutputPanel) -> None:
        clear_screen()
        print(panel(widget.title, widget.lines[-20:] + ['', 'Command: run <shell command> | clear'], self.theme))
        raw = read_command(color_line('Terminal: ', self.theme.text)).strip()
        if raw == 'clear':
            widget.clear()
        elif raw.startswith('run '):
            widget.run_command(raw.split(' ', 1)[1], shell=True)
            animate_once('Starting process', self.theme)

    def _context_menu_dialog(self, widget: ContextMenu) -> None:
        clear_screen()
        print(box(widget.title, 'Choose an action', self.theme))
        for idx, action in enumerate(widget.actions, start=1):
            danger = ' !' if action.dangerous else ''
            print(color_line(f'{idx}. {action.label}{danger}', self.theme.text))
        raw = read_command(color_line('Action: ', self.theme.text)).strip()
        if raw.isdigit():
            i = int(raw) - 1
            if 0 <= i < len(widget.actions):
                action = widget.actions[i]
                if action.dangerous and not self.confirm('Dangerous action', f'Run action "{action.label}"?'):
                    return
                widget.invoke(i)

    def _detect_resize(self) -> None:
        size = term_size()
        if size != self._last_size:
            self._last_size = size
            self.notify(f'Resize detected: {size[0]}x{size[1]}', tone='info')
            self.event_bus.emit('window.resized', size)

    def _widget_icon(self, widget: Widget) -> str:
        if widget.icon:
            return widget.icon
        mapping = {
            'button': 'button', 'toggle': 'toggle_on', 'input': 'input', 'select': 'select', 'multiselect': 'multiselect',
            'text': 'text', 'paragraph': 'paragraph', 'divider': 'divider', 'badge': 'badge', 'progress': 'progress', 'table': 'table',
            'status_table': 'status_table', 'logger': 'logger', 'log_viewer': 'log_viewer', 'command_palette': 'command_palette',
            'list': 'list', 'tree': 'tree', 'clock': 'clock', 'form': 'form', 'voice_command': 'voice', 'custom': 'plugin',
            'context_menu': 'context', 'settings_page': 'settings', 'file_picker': 'file', 'terminal_output': 'terminal', 'json_viewer': 'json',
            'keyvalue': 'stats', 'stat_card': 'stats', 'subtabs': 'subtabs', 'assistant_panel': 'assistant', 'transcript_panel': 'transcript'
        }
        return self.icon(mapping.get(widget.kind, 'info'))

    def _render_widget(self, number: int, widget: Widget, focused: bool = False) -> str:
        prefix = self._widget_icon(widget)
        disabled = 'disabled' if widget.disabled else 'ready'
        desc = widget.description or 'No description provided.'
        if isinstance(widget, Divider):
            return color_line(divider(widget.title, self.theme), self.theme.muted)
        title = f'{prefix} {widget.title}'
        lines = [f'State: {disabled}']
        tone = 'primary'
        if isinstance(widget, Button):
            tone = 'danger' if widget.dangerous else 'primary'
            meta = []
            if widget.hotkey:
                meta.append(f'hotkey={widget.hotkey}')
            if widget.dangerous:
                meta.append('dangerous')
            lines = [f'Action button • {", ".join(meta) if meta else "click to run"}', desc]
        elif isinstance(widget, Toggle):
            tone = 'success' if widget.value else 'muted'
            lines = [f'Value: {"ON" if widget.value else "OFF"}', desc]
        elif isinstance(widget, Input):
            shown = widget.display_value() or widget.placeholder or 'empty'
            typed = f' ({widget.field_type})' if widget.field_type else ''
            lines = [f'Input{typed}: {shown}', desc]
            if widget.last_error:
                lines.append(f'Validation: {widget.last_error}')
                tone = 'warning'
        elif isinstance(widget, Select):
            lines = [f'Selected: {widget.value or "none"}', f'Options: {len(widget.options)}', desc]
        elif isinstance(widget, MultiSelect):
            lines = [f'Picked: {", ".join(widget.selected) or "none"}', f'Options: {len(widget.options)}', desc]
        elif isinstance(widget, Badge):
            tone = widget.tone if hasattr(self.theme, widget.tone) else 'primary'
            lines = [f'Badge value: {widget.value}', desc]
        elif isinstance(widget, Progress):
            percent = int((widget.value / widget.maximum) * 100) if widget.maximum else 0
            bar = '█' * max(1, percent // 10) + '·' * (10 - max(1, percent // 10)) if percent else '··········'
            tone = 'success' if percent >= 70 else 'warning' if percent >= 35 else 'primary'
            lines = [f'{bar} {percent}%', f'{widget.value}/{widget.maximum}', desc]
        elif isinstance(widget, (Table, StatusTable)):
            preview = widget.page_rows()[:3]
            lines = [f'Rows: {len(widget.visible_rows())}/{len(widget.rows)} • Page {widget.current_page}/{widget.max_pages()}']
            for _, row in preview:
                lines.append(' • ' + ' | '.join(row[:4]))
            if widget.search_query:
                lines.append(f'Search: {widget.search_query}')
            lines.append(desc)
        elif isinstance(widget, LogViewer):
            last = widget.visible_entries()[-1]['message'] if widget.visible_entries() else 'No visible logs yet.'
            lines = [f'Visible logs: {len(widget.visible_entries())}', f'Latest: {last}', desc]
        elif isinstance(widget, Logger):
            last = widget.entries[-1]['message'] if widget.entries else 'No logs yet.'
            lines = [f'Entries: {len(widget.entries)}', f'Latest: {last}', desc]
        elif isinstance(widget, TerminalOutputPanel):
            last = widget.lines[-1] if widget.lines else 'No terminal output yet.'
            lines = [f'Captured lines: {len(widget.lines)}', f'Last: {last}', desc]
        elif isinstance(widget, ListWidget):
            lines = [f'Selected: {widget.selected_item or "none"}', f'Items: {len(widget.items)}', desc]
        elif isinstance(widget, TreeWidget):
            lines = [f'Selected node: {widget.selected_label() or "none"}', desc]
        elif isinstance(widget, Clock):
            tone = 'accent'
            lines = [f'{widget.prefix}{widget.current_text()} {widget.timezone_label}'.strip(), desc]
        elif isinstance(widget, KeyValue):
            preview = [f'{k} = {v}' for k, v in list(widget.items.items())[:3]]
            lines = preview + [desc]
        elif isinstance(widget, StatCard):
            tone = widget.tone if hasattr(self.theme, widget.tone) else 'primary'
            lines = [f'Value: {widget.value}', f'Delta: {widget.delta or "steady"}', desc]
        elif isinstance(widget, JSONViewer):
            lines = [f'JSON lines: {len(widget.text().splitlines())}', desc]
        elif isinstance(widget, FilePicker):
            lines = [f'Root: {widget.root}', f'Selected: {widget.selected_path or "none"}', desc]
        elif isinstance(widget, Form):
            lines = [f'Visible fields: {len(widget.visible_fields())}', desc]
        elif isinstance(widget, SettingPage):
            lines = [f'Entries: {len(widget.entries)}', desc]
        elif isinstance(widget, SubTabView):
            current = widget.current.title if widget.current else 'none'
            lines = [f'Current subtab: {current}', f'Tabs: {len(widget.tabs)}', desc]
        elif isinstance(widget, ContextMenu):
            lines = [f'Actions: {len(widget.actions)}', desc]
        elif isinstance(widget, VoiceCommand):
            lines = [f'Mode: {widget.mode}', f'Wake word: {widget.wake_word or "off"} • rules={len(widget.rules)}', desc]
        elif isinstance(widget, AssistantPanel):
            manager = widget.manager or self.assistant
            lines = manager.transcript_lines(2) + [f'Intent: {manager.last_intent} • confidence={manager.last_confidence:.2f}', f'Voice: {manager.voice_profile} • wake={manager.wake_word if manager.wake_word_enabled else "off"}', desc]
            tone = 'accent'
        elif isinstance(widget, TranscriptPanel):
            lines = widget.preview_lines() + [desc]
        elif isinstance(widget, Text):
            lines = wrap_block(widget.body or desc, 72)[:3]
        elif isinstance(widget, Paragraph):
            lines = wrap_block(widget.body or desc, 72)[:4]
        elif isinstance(widget, CustomWidget):
            spec = self.widget_registry.specs.get(widget.custom_type)
            if spec:
                return spec.render(self, widget, number)
        return widget_card(title, lines, self.theme, focused=focused, tone=tone, index=number)

    def _draw(self) -> None:
        clear_screen()
        subtitle = self.config.subtitle or 'beautiful terminal toolkit'
        print(box(f'{self.icon("app")} {self.config.title}', subtitle, self.theme))
        print(tab_strip([tab.title for tab in self.tabs], self._active_tab_index, self.theme))
        meta_items = [
            f'Theme {self.theme.name}',
            f'Icons {self.icon_pack.name}',
            f'Density {self.config.density}',
            f'Widgets {len(self._indexed_widgets())}',
            f'Focus {self._focus_index + 1 if self._indexed_widgets() else 0}',
        ]
        if self.config.accessibility_mode:
            meta_items.append('Accessibility on')
        if self.config.mouse_support:
            meta_items.append('Mouse on')
        print(stat_row(meta_items, self.theme))
        if self._notifications:
            print()
            notes = [f'{tone.upper()}: {message}' for tone, message in self._notifications[-3:]]
            print(panel(f'{self.icon("notice")} Live Notifications', notes, self.theme, tone='accent'))
        print()
        number = 1
        for section in self.current_tab.sections:
            print(section_header(f'{self.icon("section")} {section.title}', self.theme, count=len(section.widgets)))
            for widget in section.widgets:
                print(self._render_widget(number, widget, focused=(number - 1 == self._focus_index)))
                if self.config.density == 'cozy':
                    print()
                number += 1
            print()
        print(footer_bar(['number', 'j/k', ':left/:right', 'a', '/search', 'x', 'themes', 'theme <name>', 'q'], self.theme))


class App:
    def __init__(self, theme: str = 'dark', icon_pack: str = 'emoji', density: str = 'cozy', accessibility_mode: bool = False, mouse_support: bool = False):
        self.theme = theme
        self.icon_pack = icon_pack
        self.density = density
        self.accessibility_mode = accessibility_mode
        self.mouse_support = mouse_support

    def create_window(self, title: str, subtitle: str = '', *, theme: str | None = None, icon_pack: str | None = None, density: str | None = None, accessibility_mode: bool | None = None, mouse_support: bool | None = None) -> Window:
        return Window(WindowConfig(
            title=title,
            subtitle=subtitle,
            theme=theme or self.theme,
            icon_pack=icon_pack or self.icon_pack,
            density=density or self.density,
            accessibility_mode=self.accessibility_mode if accessibility_mode is None else accessibility_mode,
            mouse_support=self.mouse_support if mouse_support is None else mouse_support,
        ))
