from __future__ import annotations

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

from .assistant import AssistantManager

from .clipboard import ClipboardHelper
from .validators import Validator, validator_for
from .voice import VoiceEngine, VoiceRouter, VoiceRule, find_best_phrase, normalize_spoken_text

Callback = Callable[..., None]
Condition = Callable[[dict[str, Any]], bool]


@dataclass
class Widget:
    title: str
    kind: str = field(init=False, default='widget')
    disabled: bool = False
    description: str = ''
    icon: str = ''
    visible_when: Optional[Condition] = None

    def as_state(self):
        return None

    def is_visible(self, payload: dict[str, Any] | None = None) -> bool:
        if not self.visible_when:
            return True
        try:
            return bool(self.visible_when(payload or {}))
        except Exception:
            return True


@dataclass
class Text(Widget):
    body: str = ''
    kind: str = field(init=False, default='text')


@dataclass
class Paragraph(Widget):
    body: str = ''
    kind: str = field(init=False, default='paragraph')


@dataclass
class Divider(Widget):
    kind: str = field(init=False, default='divider')


@dataclass
class Button(Widget):
    callback: Optional[Callback] = None
    hotkey: str = ''
    close_on_press: bool = False
    dangerous: bool = False
    kind: str = field(init=False, default='button')

    def trigger(self) -> None:
        if self.callback:
            self.callback()


@dataclass
class Toggle(Widget):
    value: bool = False
    callback: Optional[Callable[[bool], None]] = None
    kind: str = field(init=False, default='toggle')

    def flip(self) -> None:
        self.value = not self.value
        if self.callback:
            self.callback(self.value)

    def as_state(self):
        return self.value


@dataclass
class Input(Widget):
    placeholder: str = ''
    value: str = ''
    callback: Optional[Callable[[str], None]] = None
    password: bool = False
    multiline: bool = False
    validators: list[Validator] = field(default_factory=list)
    required: bool = False
    last_error: str = ''
    field_type: str = ''
    mask_char: str = '*'
    kind: str = field(init=False, default='input')

    def __post_init__(self) -> None:
        validator = validator_for(self.field_type)
        if validator and validator not in self.validators:
            self.validators.append(validator)

    def set_value(self, value: str) -> None:
        self.value = value
        if self.callback:
            self.callback(value)

    def display_value(self) -> str:
        if self.password and self.value:
            return self.mask_char * len(self.value)
        return self.value

    def validate(self) -> tuple[bool, str]:
        if self.required and not str(self.value).strip():
            self.last_error = 'This field is required.'
            return False, self.last_error
        for validator in self.validators:
            ok, message = validator(self.value)
            if not ok:
                self.last_error = message
                return False, message
        self.last_error = ''
        return True, ''

    def as_state(self):
        return self.value


@dataclass
class Select(Widget):
    options: list[str] = field(default_factory=list)
    value: str = ''
    callback: Optional[Callable[[str], None]] = None
    validators: list[Validator] = field(default_factory=list)
    required: bool = False
    last_error: str = ''
    kind: str = field(init=False, default='select')

    def set_value(self, value: str) -> None:
        if value in self.options:
            self.value = value
            if self.callback:
                self.callback(value)

    def validate(self) -> tuple[bool, str]:
        if self.required and not str(self.value).strip():
            self.last_error = 'Please choose a value.'
            return False, self.last_error
        for validator in self.validators:
            ok, message = validator(self.value)
            if not ok:
                self.last_error = message
                return False, message
        self.last_error = ''
        return True, ''

    def as_state(self):
        return self.value


@dataclass
class MultiSelect(Widget):
    options: list[str] = field(default_factory=list)
    selected: list[str] = field(default_factory=list)
    callback: Optional[Callable[[list[str]], None]] = None
    selected_index: int = 0
    kind: str = field(init=False, default='multiselect')

    def toggle_option(self, value: str) -> None:
        if value in self.selected:
            self.selected.remove(value)
        elif value in self.options:
            self.selected.append(value)
        if self.callback:
            self.callback(list(self.selected))

    def move_selection(self, delta: int) -> None:
        if not self.options:
            self.selected_index = 0
            return
        self.selected_index = max(0, min(self.selected_index + delta, len(self.options) - 1))

    def current(self) -> str:
        return self.options[self.selected_index] if self.options else ''

    def as_state(self):
        return {'selected': list(self.selected), 'selected_index': self.selected_index}


@dataclass
class Badge(Widget):
    value: str = ''
    tone: str = 'info'
    kind: str = field(init=False, default='badge')

    def set_value(self, value: str) -> None:
        self.value = value

    def as_state(self):
        return self.value


@dataclass
class Progress(Widget):
    value: int = 0
    maximum: int = 100
    show_percent: bool = True
    callback: Optional[Callable[[int], None]] = None
    kind: str = field(init=False, default='progress')

    def set_value(self, value: int) -> None:
        self.value = max(0, min(int(value), self.maximum))
        if self.callback:
            self.callback(self.value)

    def as_state(self):
        return self.value


@dataclass
class ContextAction:
    label: str
    callback: Optional[Callable[[], None]] = None
    dangerous: bool = False


@dataclass
class ContextMenu(Widget):
    actions: list[ContextAction] = field(default_factory=list)
    kind: str = field(init=False, default='context_menu')

    def add_action(self, label: str, callback: Optional[Callable[[], None]] = None, dangerous: bool = False) -> ContextAction:
        action = ContextAction(label=label, callback=callback, dangerous=dangerous)
        self.actions.append(action)
        return action

    def invoke(self, index: int) -> bool:
        if 0 <= index < len(self.actions):
            action = self.actions[index]
            if action.callback:
                action.callback()
            return True
        return False


@dataclass
class Table(Widget):
    columns: list[str] = field(default_factory=list)
    rows: list[list[str]] = field(default_factory=list)
    striped: bool = False
    selected_row: int = 0
    searchable: bool = False
    search_query: str = ''
    sortable: bool = True
    sort_column: int = 0
    sort_reverse: bool = False
    page_size: int = 8
    current_page: int = 1
    editable: bool = False
    filters: dict[str, str] = field(default_factory=dict)
    kind: str = field(init=False, default='table')

    def add_row(self, *values: object) -> None:
        self.rows.append([str(v) for v in values])
        self.selected_row = max(0, min(self.selected_row, len(self.page_rows()) - 1))

    def clear_rows(self) -> None:
        self.rows.clear()
        self.selected_row = 0
        self.current_page = 1

    def _filtered_rows(self) -> list[tuple[int, list[str]]]:
        query = normalize_spoken_text(self.search_query)
        indexed = list(enumerate(self.rows))
        filtered: list[tuple[int, list[str]]] = []
        for idx, row in indexed:
            haystack = normalize_spoken_text(' '.join(map(str, row)))
            if self.searchable and query and query not in haystack:
                continue
            passed = True
            for key, expected in self.filters.items():
                if key in self.columns:
                    col_idx = self.columns.index(key)
                    if normalize_spoken_text(row[col_idx]) != normalize_spoken_text(expected):
                        passed = False
                        break
            if passed:
                filtered.append((idx, row))
        if self.sortable and self.columns:
            filtered.sort(key=lambda item: item[1][self.sort_column] if self.sort_column < len(item[1]) else '', reverse=self.sort_reverse)
        return filtered

    def visible_rows(self) -> list[tuple[int, list[str]]]:
        return self._filtered_rows()

    def page_rows(self) -> list[tuple[int, list[str]]]:
        visible = self.visible_rows()
        start = max(0, (self.current_page - 1) * self.page_size)
        end = start + self.page_size
        return visible[start:end]

    def max_pages(self) -> int:
        visible = self.visible_rows()
        if not visible:
            return 1
        return max(1, ((len(visible) - 1) // self.page_size) + 1)

    def next_page(self) -> None:
        self.current_page = min(self.max_pages(), self.current_page + 1)
        self.selected_row = 0

    def prev_page(self) -> None:
        self.current_page = max(1, self.current_page - 1)
        self.selected_row = 0

    def move_selection(self, delta: int) -> None:
        rows = self.page_rows()
        if not rows:
            self.selected_row = 0
            return
        self.selected_row = max(0, min(self.selected_row + delta, len(rows) - 1))

    def selected_source_index(self) -> int | None:
        rows = self.page_rows()
        if not rows:
            return None
        return rows[self.selected_row][0]

    def selected_row_values(self) -> list[str]:
        rows = self.page_rows()
        if not rows:
            return []
        return list(rows[self.selected_row][1])

    def delete_selected_row(self) -> bool:
        idx = self.selected_source_index()
        if idx is None:
            return False
        self.rows.pop(idx)
        self.selected_row = max(0, min(self.selected_row, len(self.page_rows()) - 1))
        return True

    def set_search_query(self, query: str) -> None:
        self.search_query = query.strip()
        self.current_page = 1
        self.selected_row = 0

    def clear_search(self) -> None:
        self.search_query = ''
        self.current_page = 1
        self.selected_row = 0

    def sort_by(self, column_name: str | int, reverse: bool | None = None) -> None:
        if isinstance(column_name, int):
            self.sort_column = max(0, min(column_name, max(0, len(self.columns) - 1)))
        elif column_name in self.columns:
            self.sort_column = self.columns.index(column_name)
        if reverse is None:
            self.sort_reverse = not self.sort_reverse
        else:
            self.sort_reverse = reverse

    def set_filter(self, column_name: str, value: str) -> None:
        if value:
            self.filters[column_name] = value
        else:
            self.filters.pop(column_name, None)
        self.current_page = 1
        self.selected_row = 0

    def clear_filters(self) -> None:
        self.filters.clear()
        self.current_page = 1
        self.selected_row = 0

    def edit_cell(self, row_index: int, column_name: str | int, value: str) -> bool:
        if not self.editable:
            return False
        if isinstance(column_name, int):
            col_index = column_name
        else:
            if column_name not in self.columns:
                return False
            col_index = self.columns.index(column_name)
        if 0 <= row_index < len(self.rows) and 0 <= col_index < len(self.rows[row_index]):
            self.rows[row_index][col_index] = str(value)
            return True
        return False

    def as_state(self):
        return {
            'columns': list(self.columns), 'rows': [list(r) for r in self.rows], 'selected_row': self.selected_row,
            'search_query': self.search_query, 'searchable': self.searchable, 'sort_column': self.sort_column,
            'sort_reverse': self.sort_reverse, 'page_size': self.page_size, 'current_page': self.current_page,
            'filters': dict(self.filters), 'editable': self.editable,
        }


@dataclass
class Logger(Widget):
    entries: list[dict[str, str]] = field(default_factory=list)
    max_entries: int = 200
    kind: str = field(init=False, default='logger')

    def log(self, level: str, message: str) -> None:
        self.entries.append({'level': level.upper(), 'message': message})
        self.entries[:] = self.entries[-self.max_entries:]

    def clear(self) -> None:
        self.entries.clear()

    def as_state(self):
        return list(self.entries)


@dataclass
class LogViewer(Logger):
    level_filter: str = ''
    search_query: str = ''
    kind: str = field(init=False, default='log_viewer')

    def visible_entries(self) -> list[dict[str, str]]:
        query = normalize_spoken_text(self.search_query)
        result = []
        for entry in self.entries:
            if self.level_filter and entry['level'] != self.level_filter.upper():
                continue
            if query and query not in normalize_spoken_text(entry['message']):
                continue
            result.append(entry)
        return result


@dataclass
class TerminalOutputPanel(Widget):
    lines: list[str] = field(default_factory=list)
    process: subprocess.Popen | None = None
    max_lines: int = 300
    kind: str = field(init=False, default='terminal_output')

    def run_command(self, command: list[str] | str, cwd: str | None = None, shell: bool = False) -> None:
        self.lines.append(f'$ {command}')
        self.process = subprocess.Popen(command, cwd=cwd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
        threading.Thread(target=self._pump, daemon=True).start()

    def _pump(self) -> None:
        assert self.process is not None
        if self.process.stdout is None:
            return
        for line in self.process.stdout:
            self.lines.append(line.rstrip())
            self.lines[:] = self.lines[-self.max_lines:]

    def clear(self) -> None:
        self.lines.clear()

    def as_state(self):
        return list(self.lines)


@dataclass
class CommandPalette(Widget):
    commands: list[tuple[str, str, Optional[Callback]]] = field(default_factory=list)
    kind: str = field(init=False, default='command_palette')

    def register(self, name: str, description: str, callback: Optional[Callback]) -> None:
        self.commands.append((name, description, callback))


@dataclass
class ListWidget(Widget):
    items: list[str] = field(default_factory=list)
    selected_index: int = 0
    callback: Optional[Callable[[str], None]] = None
    kind: str = field(init=False, default='list')

    def move_selection(self, delta: int) -> None:
        if not self.items:
            self.selected_index = 0
            return
        self.selected_index = max(0, min(self.selected_index + delta, len(self.items) - 1))

    @property
    def selected_item(self) -> str:
        return self.items[self.selected_index] if self.items else ''

    def activate(self) -> None:
        if self.callback and self.items:
            self.callback(self.selected_item)

    def as_state(self):
        return {'items': list(self.items), 'selected_index': self.selected_index}


@dataclass
class TreeNode:
    label: str
    children: list['TreeNode'] = field(default_factory=list)
    expanded: bool = True
    meta: dict[str, Any] = field(default_factory=dict)


@dataclass
class TreeWidget(Widget):
    nodes: list[TreeNode] = field(default_factory=list)
    selected_index: int = 0
    callback: Optional[Callable[[str], None]] = None
    kind: str = field(init=False, default='tree')

    def flatten(self) -> list[tuple[int, TreeNode]]:
        lines: list[tuple[int, TreeNode]] = []
        def walk(node: TreeNode, depth: int) -> None:
            lines.append((depth, node))
            if node.expanded:
                for child in node.children:
                    walk(child, depth + 1)
        for node in self.nodes:
            walk(node, 0)
        return lines

    def move_selection(self, delta: int) -> None:
        flat = self.flatten()
        if not flat:
            self.selected_index = 0
            return
        self.selected_index = max(0, min(self.selected_index + delta, len(flat) - 1))

    def toggle_selected(self) -> None:
        flat = self.flatten()
        if flat:
            flat[self.selected_index][1].expanded = not flat[self.selected_index][1].expanded

    def selected_label(self) -> str:
        flat = self.flatten()
        return flat[self.selected_index][1].label if flat else ''

    def activate(self) -> None:
        if self.callback:
            self.callback(self.selected_label())

    def as_state(self):
        def dump(node: TreeNode):
            return {'label': node.label, 'expanded': node.expanded, 'meta': dict(node.meta), 'children': [dump(child) for child in node.children]}
        return {'selected_index': self.selected_index, 'nodes': [dump(node) for node in self.nodes]}


@dataclass
class Clock(Widget):
    format_24h: bool = True
    show_seconds: bool = True
    timezone_label: str = 'Local'
    prefix: str = ''
    kind: str = field(init=False, default='clock')

    def current_text(self) -> str:
        fmt = '%H:%M:%S' if self.format_24h else '%I:%M:%S %p'
        if not self.show_seconds:
            fmt = '%H:%M' if self.format_24h else '%I:%M %p'
        return time.strftime(fmt)


@dataclass
class KeyValue(Widget):
    items: dict[str, str] = field(default_factory=dict)
    kind: str = field(init=False, default='keyvalue')

    def set(self, key: str, value: str) -> None:
        self.items[str(key)] = str(value)

    def as_state(self):
        return dict(self.items)


@dataclass
class StatCard(Widget):
    value: str = ''
    tone: str = 'info'
    delta: str = ''
    kind: str = field(init=False, default='stat_card')

    def as_state(self):
        return {'value': self.value, 'tone': self.tone, 'delta': self.delta}


@dataclass
class JSONViewer(Widget):
    data: Any = field(default_factory=dict)
    collapsed: bool = False
    kind: str = field(init=False, default='json_viewer')

    def text(self) -> str:
        return json.dumps(self.data, indent=2, ensure_ascii=False)


@dataclass
class FilePicker(Widget):
    root: str = '.'
    selected_path: str = ''
    patterns: list[str] = field(default_factory=lambda: ['*'])
    callback: Optional[Callable[[str], None]] = None
    kind: str = field(init=False, default='file_picker')

    def choices(self) -> list[str]:
        root = Path(self.root)
        found: list[str] = []
        if not root.exists():
            return found
        for pattern in self.patterns:
            for item in sorted(root.glob(pattern)):
                found.append(str(item))
        return found

    def select(self, path: str) -> None:
        self.selected_path = path
        if self.callback:
            self.callback(path)

    def as_state(self):
        return self.selected_path


@dataclass
class Form(Widget):
    fields: list[Widget] = field(default_factory=list)
    submit_callback: Optional[Callable[[dict[str, Any]], None]] = None
    kind: str = field(init=False, default='form')

    def values(self) -> dict[str, Any]:
        payload: dict[str, Any] = {}
        for item in self.fields:
            if hasattr(item, 'value'):
                payload[item.title] = getattr(item, 'value')
            elif isinstance(item, MultiSelect):
                payload[item.title] = list(item.selected)
        return payload

    def visible_fields(self) -> list[Widget]:
        payload = self.values()
        return [field for field in self.fields if field.is_visible(payload)]

    def validate(self) -> tuple[bool, list[str]]:
        errors: list[str] = []
        for field in self.visible_fields():
            validator = getattr(field, 'validate', None)
            if callable(validator):
                ok, message = validator()
                if not ok:
                    errors.append(f'{field.title}: {message}')
        return not errors, errors

    def submit(self) -> tuple[bool, list[str]]:
        ok, errors = self.validate()
        if ok and self.submit_callback:
            self.submit_callback(self.values())
        return ok, errors

    def export_preset(self, path: str | Path) -> Path:
        path = Path(path)
        path.write_text(json.dumps(self.values(), indent=2), encoding='utf-8')
        return path

    def import_preset(self, path: str | Path) -> bool:
        path = Path(path)
        if not path.exists():
            return False
        payload = json.loads(path.read_text(encoding='utf-8'))
        for field in self.fields:
            if field.title not in payload:
                continue
            value = payload[field.title]
            if isinstance(field, MultiSelect):
                field.selected = [x for x in value if x in field.options]
            elif hasattr(field, 'set_value'):
                field.set_value(str(value))
        return True


@dataclass
class SettingPage(Widget):
    entries: list[Widget] = field(default_factory=list)
    save_callback: Optional[Callable[[dict[str, Any]], None]] = None
    kind: str = field(init=False, default='settings_page')

    def values(self) -> dict[str, Any]:
        payload: dict[str, Any] = {}
        for entry in self.entries:
            if hasattr(entry, 'value'):
                payload[entry.title] = getattr(entry, 'value')
            elif isinstance(entry, Toggle):
                payload[entry.title] = entry.value
            elif isinstance(entry, MultiSelect):
                payload[entry.title] = list(entry.selected)
        return payload

    def save(self) -> dict[str, Any]:
        payload = self.values()
        if self.save_callback:
            self.save_callback(payload)
        return payload


@dataclass
class SubTab:
    title: str
    widgets: list[Widget] = field(default_factory=list)


@dataclass
class SubTabView(Widget):
    tabs: list[SubTab] = field(default_factory=list)
    active_index: int = 0
    kind: str = field(init=False, default='subtabs')

    def add_tab(self, title: str, widgets: list[Widget] | None = None) -> SubTab:
        tab = SubTab(title=title, widgets=list(widgets or []))
        self.tabs.append(tab)
        return tab

    @property
    def current(self) -> SubTab | None:
        if not self.tabs:
            return None
        return self.tabs[self.active_index]

    def next_tab(self) -> None:
        if self.tabs:
            self.active_index = (self.active_index + 1) % len(self.tabs)


@dataclass
class CustomWidget(Widget):
    custom_type: str = 'custom'
    data: dict[str, Any] = field(default_factory=dict)
    callback: Optional[Callable[['CustomWidget'], None]] = None
    kind: str = field(init=False, default='custom')

    def as_state(self):
        return {'custom_type': self.custom_type, 'data': dict(self.data)}


@dataclass
class StatusTable(Table):
    root_path: str = ''
    include_hidden: bool = False
    scanner: Optional[Callable[[], list[list[str]]]] = None
    kind: str = field(init=False, default='status_table')

    def refresh(self) -> None:
        if self.scanner:
            self.rows = [list(map(str, row)) for row in self.scanner()]
            self.selected_row = 0


@dataclass
class VoiceCommand(Widget):
    placeholder: str = 'Say or type a command'
    callback: Optional[Callable[[str], None]] = None
    prefer_microphone: bool = True
    timeout: int = 5
    phrase_time_limit: int = 8
    last_command: str = ''
    mode: str = 'free'
    fuzzy_threshold: float = 0.68
    command_callback: Optional[Callable[[str, str], None]] = None
    rules: list[VoiceRule] = field(default_factory=list)
    wake_word: str = ''
    cooldown_seconds: float = 0.0
    multilingual_phrase_pack: str = 'en'
    kind: str = field(init=False, default='voice_command')

    def register_phrase(self, name: str, *phrases: str, callback: Optional[Callable[[str, str], None]] = None, description: str = '', confirm: bool = False, threshold: float | None = None) -> VoiceRule:
        rule = VoiceRule(name=name, phrases=[p for p in phrases if p], callback=callback, description=description, confirm=confirm, threshold=threshold or self.fuzzy_threshold)
        self.rules.append(rule)
        return rule

    def set_mode(self, mode: str) -> None:
        mode = (mode or 'free').strip().lower()
        if mode not in {'free', 'commands', 'hybrid'}:
            mode = 'free'
        self.mode = mode

    def available_commands(self) -> list[str]:
        return [rule.name for rule in self.rules]

    def match_rule(self, command: str) -> tuple[Optional[VoiceRule], str, float]:
        if not self.rules:
            return None, '', 0.0
        phrase_to_rule: dict[str, VoiceRule] = {}
        phrases: list[str] = []
        for rule in self.rules:
            for phrase in rule.all_phrases():
                normalized = normalize_spoken_text(phrase)
                if normalized:
                    phrase_to_rule[normalized] = rule
                    phrases.append(normalized)
        if not phrases:
            return None, '', 0.0
        best_phrase, score = find_best_phrase(command, phrases)
        if not best_phrase:
            return None, '', 0.0
        rule = phrase_to_rule.get(best_phrase)
        if not rule or score < rule.threshold:
            return None, best_phrase, score
        return rule, best_phrase, score

    def submit(self, command: str, confirm_handler: Optional[Callable[[str], bool]] = None) -> dict[str, Any]:
        self.last_command = command
        router = VoiceRouter(wake_word=self.wake_word, cooldown_seconds=self.cooldown_seconds, language_pack=self.multilingual_phrase_pack)
        router.free_handler = self.callback
        router.confirm_handler = confirm_handler
        for rule in self.rules:
            router.register(rule.name, *rule.phrases, callback=rule.callback or self.command_callback, description=rule.description, confirm=rule.confirm, threshold=rule.threshold)
        mode = (self.mode or 'free').lower()
        if mode == 'free':
            if self.callback:
                self.callback(command)
            return {'mode': mode, 'command': command, 'matched': False}
        result = router.execute(command)
        if mode == 'commands':
            return result
        if not result.get('ok') and self.callback:
            self.callback(command)
            result['fallback'] = True
        return result

    def as_state(self):
        return {'last_command': self.last_command, 'prefer_microphone': self.prefer_microphone, 'mode': self.mode}



@dataclass
class TranscriptPanel(Widget):
    manager: AssistantManager | None = None
    max_preview: int = 6
    kind: str = field(init=False, default='transcript_panel')

    def preview_lines(self) -> list[str]:
        if not self.manager:
            return ['(no assistant attached)']
        return self.manager.transcript_lines(self.max_preview)


@dataclass
class AssistantPanel(Widget):
    manager: AssistantManager | None = None
    prefer_microphone: bool = True
    timeout: int = 5
    phrase_time_limit: int = 8
    kind: str = field(init=False, default='assistant_panel')

    def status_lines(self) -> list[str]:
        if not self.manager:
            return ['Assistant missing']
        return [
            f'Enabled: {self.manager.enabled}',
            f'Listening: {self.manager.listen_enabled} • Voice reply: {self.manager.voice_reply_enabled}',
            f'Wake word: {self.manager.wake_word if self.manager.wake_word_enabled else "off"}',
            f'Voice: {self.manager.voice_profile} • Persona: {self.manager.persona}',
        ]

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

    def _append(self, widget: Widget):
        self.widgets.append(widget)
        return widget

    def add_text(self, title: str, body: str = '', description: str = '', *, icon: str = '') -> Text:
        return self._append(Text(title=title, body=body, description=description, icon=icon))

    def add_paragraph(self, title: str, body: str, description: str = '', *, icon: str = '') -> Paragraph:
        return self._append(Paragraph(title=title, body=body, description=description, icon=icon))

    def add_divider(self, title: str = '', *, icon: str = '') -> Divider:
        return self._append(Divider(title=title, icon=icon))

    def add_button(self, title: str, callback: Optional[Callback] = None, *, hotkey: str = '', description: str = '', icon: str = '', close_on_press: bool = False, dangerous: bool = False) -> Button:
        return self._append(Button(title=title, callback=callback, hotkey=hotkey, description=description, icon=icon, close_on_press=close_on_press, dangerous=dangerous))

    def add_toggle(self, title: str, default: bool = False, callback: Optional[Callable[[bool], None]] = None, *, description: str = '', icon: str = '') -> Toggle:
        return self._append(Toggle(title=title, value=default, callback=callback, description=description, icon=icon))

    def add_input(self, title: str, placeholder: str = '', callback: Optional[Callable[[str], None]] = None, *, password: bool = False, multiline: bool = False, description: str = '', icon: str = '', validators: Optional[list[Validator]] = None, required: bool = False, field_type: str = '', mask_char: str = '*', visible_when: Condition | None = None) -> Input:
        return self._append(Input(title=title, placeholder=placeholder, callback=callback, password=password, multiline=multiline, description=description, icon=icon, validators=list(validators or []), required=required, field_type=field_type, mask_char=mask_char, visible_when=visible_when))

    def add_select(self, title: str, options: list[str], default: str = '', callback: Optional[Callable[[str], None]] = None, *, description: str = '', icon: str = '', validators: Optional[list[Validator]] = None, required: bool = False, visible_when: Condition | None = None) -> Select:
        return self._append(Select(title=title, options=list(options), value=default or (options[0] if options else ''), callback=callback, description=description, icon=icon, validators=list(validators or []), required=required, visible_when=visible_when))

    def add_multiselect(self, title: str, options: list[str], default: Optional[list[str]] = None, callback: Optional[Callable[[list[str]], None]] = None, *, description: str = '', icon: str = '') -> MultiSelect:
        return self._append(MultiSelect(title=title, options=list(options), selected=list(default or []), callback=callback, description=description, icon=icon))

    def add_badge(self, title: str, value: str, tone: str = 'info', *, description: str = '', icon: str = '') -> Badge:
        return self._append(Badge(title=title, value=value, tone=tone, description=description, icon=icon))

    def add_progress(self, title: str, value: int = 0, maximum: int = 100, callback: Optional[Callable[[int], None]] = None, *, show_percent: bool = True, description: str = '', icon: str = '') -> Progress:
        return self._append(Progress(title=title, value=value, maximum=maximum, callback=callback, show_percent=show_percent, description=description, icon=icon))

    def add_context_menu(self, title: str, *, description: str = '', icon: str = '') -> ContextMenu:
        return self._append(ContextMenu(title=title, description=description, icon=icon))

    def add_table(self, title: str, columns: list[str], rows: Optional[list[list[str]]] = None, *, striped: bool = False, searchable: bool = False, search_query: str = '', description: str = '', icon: str = '', sortable: bool = True, page_size: int = 8, editable: bool = False) -> Table:
        return self._append(Table(title=title, columns=list(columns), rows=[list(r) for r in (rows or [])], striped=striped, searchable=searchable, search_query=search_query, description=description, icon=icon, sortable=sortable, page_size=page_size, editable=editable))

    def add_logger(self, title: str = 'Logs', *, max_entries: int = 200, description: str = '', icon: str = '') -> Logger:
        return self._append(Logger(title=title, max_entries=max_entries, description=description, icon=icon))

    def add_log_viewer(self, title: str = 'Log Viewer', *, max_entries: int = 200, description: str = '', icon: str = '') -> LogViewer:
        return self._append(LogViewer(title=title, max_entries=max_entries, description=description, icon=icon))

    def add_terminal_output(self, title: str = 'Terminal Output', *, description: str = '', icon: str = '') -> TerminalOutputPanel:
        return self._append(TerminalOutputPanel(title=title, description=description, icon=icon))

    def add_command_palette(self, title: str = 'Command Palette', *, description: str = '', icon: str = '') -> CommandPalette:
        return self._append(CommandPalette(title=title, description=description, icon=icon))

    def add_list(self, title: str, items: list[str], callback: Optional[Callable[[str], None]] = None, *, description: str = '', icon: str = '') -> ListWidget:
        return self._append(ListWidget(title=title, items=list(items), callback=callback, description=description, icon=icon))

    def add_tree(self, title: str, nodes: list[TreeNode], callback: Optional[Callable[[str], None]] = None, *, description: str = '', icon: str = '') -> TreeWidget:
        return self._append(TreeWidget(title=title, nodes=list(nodes), callback=callback, description=description, icon=icon))

    def add_clock(self, title: str = 'Clock', *, format_24h: bool = True, show_seconds: bool = True, timezone_label: str = 'Local', prefix: str = '', description: str = '', icon: str = '') -> Clock:
        return self._append(Clock(title=title, format_24h=format_24h, show_seconds=show_seconds, timezone_label=timezone_label, prefix=prefix, description=description, icon=icon))

    def add_keyvalue(self, title: str, items: Optional[dict[str, str]] = None, *, description: str = '', icon: str = '') -> KeyValue:
        return self._append(KeyValue(title=title, items=dict(items or {}), description=description, icon=icon))

    def add_key_value(self, title: str, value: str, *, description: str = '', icon: str = '') -> KeyValue:
        return self.add_keyvalue(title, {title: value}, description=description, icon=icon)

    def add_stat_card(self, title: str, value: str, *, tone: str = 'info', delta: str = '', description: str = '', icon: str = '') -> StatCard:
        return self._append(StatCard(title=title, value=value, tone=tone, delta=delta, description=description, icon=icon))

    def add_stat(self, title: str, value: str, *, tone: str = 'info', delta: str = '', description: str = '', icon: str = '') -> StatCard:
        return self.add_stat_card(title, value, tone=tone, delta=delta, description=description, icon=icon)

    def add_json_viewer(self, title: str, data: Any, *, description: str = '', icon: str = '') -> JSONViewer:
        return self._append(JSONViewer(title=title, data=data, description=description, icon=icon))

    def add_file_picker(self, title: str, root: str = '.', *, patterns: Optional[list[str]] = None, callback: Optional[Callable[[str], None]] = None, description: str = '', icon: str = '') -> FilePicker:
        return self._append(FilePicker(title=title, root=root, patterns=list(patterns or ['*']), callback=callback, description=description, icon=icon))

    def add_form(self, title: str, fields: list[Widget], submit_callback: Optional[Callable[[dict[str, Any]], None]] = None, *, description: str = '', icon: str = '') -> Form:
        return self._append(Form(title=title, fields=list(fields), submit_callback=submit_callback, description=description, icon=icon))

    def add_settings_page(self, title: str, entries: list[Widget], save_callback: Optional[Callable[[dict[str, Any]], None]] = None, *, description: str = '', icon: str = '') -> SettingPage:
        return self._append(SettingPage(title=title, entries=list(entries), save_callback=save_callback, description=description, icon=icon))

    def add_subtabs(self, title: str, *, description: str = '', icon: str = '') -> SubTabView:
        return self._append(SubTabView(title=title, description=description, icon=icon))

    def add_status_table(self, title: str, columns: list[str] | None = None, rows: Optional[list[list[str]]] = None, *, root_path: str = '', include_hidden: bool = False, scanner: Optional[Callable[[], list[list[str]]]] = None, searchable: bool = True, search_query: str = '', description: str = '', icon: str = '', sortable: bool = True, page_size: int = 8, editable: bool = False) -> StatusTable:
        return self._append(StatusTable(title=title, columns=list(columns or ['Name', 'Type', 'Status', 'Startup']), rows=[list(r) for r in (rows or [])], root_path=root_path, include_hidden=include_hidden, scanner=scanner, searchable=searchable, search_query=search_query, description=description, icon=icon, sortable=sortable, page_size=page_size, editable=editable))

    def add_voice_command(self, title: str = 'Voice Command', callback: Optional[Callable[[str], None]] = None, *, placeholder: str = 'Say or type a command', prefer_microphone: bool = True, timeout: int = 5, phrase_time_limit: int = 8, mode: str = 'free', command_callback: Optional[Callable[[str, str], None]] = None, fuzzy_threshold: float = 0.68, description: str = '', icon: str = '', wake_word: str = '', cooldown_seconds: float = 0.0, multilingual_phrase_pack: str = 'en') -> VoiceCommand:
        return self._append(VoiceCommand(title=title, callback=callback, placeholder=placeholder, prefer_microphone=prefer_microphone, timeout=timeout, phrase_time_limit=phrase_time_limit, mode=mode, command_callback=command_callback, fuzzy_threshold=fuzzy_threshold, description=description, icon=icon, wake_word=wake_word, cooldown_seconds=cooldown_seconds, multilingual_phrase_pack=multilingual_phrase_pack))

    def add_transcript_panel(self, title: str = 'Transcript', manager: AssistantManager | None = None, *, max_preview: int = 6, description: str = '', icon: str = '') -> TranscriptPanel:
        return self._append(TranscriptPanel(title=title, manager=manager, max_preview=max_preview, description=description, icon=icon))

    def add_assistant_panel(self, title: str = 'Assistant', manager: AssistantManager | None = None, *, prefer_microphone: bool = True, timeout: int = 5, phrase_time_limit: int = 8, description: str = '', icon: str = '') -> AssistantPanel:
        return self._append(AssistantPanel(title=title, manager=manager, prefer_microphone=prefer_microphone, timeout=timeout, phrase_time_limit=phrase_time_limit, description=description, icon=icon))

    def add_custom(self, title: str, custom_type: str, data: Optional[dict[str, Any]] = None, callback: Optional[Callable[[CustomWidget], None]] = None, *, description: str = '', icon: str = '') -> CustomWidget:
        return self._append(CustomWidget(title=title, custom_type=custom_type, data=dict(data or {}), callback=callback, description=description, icon=icon))
