"""Classic CLI live-work dock (subagents + background processes) and scoped controls; no
agent-loop state is changed. Process rows come from ``cli_process_dock``."""
from __future__ import annotations

import json
import time

from prompt_toolkit.utils import get_cwidth

from hermes_cli import cli_process_dock as procs


def _clip(value, width):
    text = ' '.join(str(value or '').split())
    text = ''.join(c for c in text if c.isprintable())
    if get_cwidth(text) <= width:
        return text
    result = ''
    for char in text:
        if get_cwidth(result + char) > max(0, width - 1):
            break
        result += char
    return result + ('…' if width else '')


class SubagentMonitor:
    def __init__(self, cli):
        self.cli = cli
        self.entries = []
        self.processes = []
        self.selected_id = None
        self._signature = None
        self._last_poll = 0
        self.app = None
        self.opening = False
        self.collapsed = False

    @property
    def roster(self):
        """Agents first, then processes — the order the dock and the full-height monitor paint."""
        return [*self.entries, *self.processes]

    @staticmethod
    def _key(row):
        return row.get('key', row.get('subagent_id'))

    @property
    def selected(self):
        return next((r for r in self.roster if self._key(r) == self.selected_id), None)

    @property
    def selected_process(self):
        row = self.selected
        return row if row and row.get('kind') == 'process' else None

    def refresh(self, now=None):
        from tools.delegate_tool_registry import _list_payload, list_active_subagents
        now = time.time() if now is None else now
        parent = getattr(self.cli, 'agent', None)
        entries = _list_payload(parent)['subagents'] if parent is not None else []
        # The scoped control-plane snapshot supplies authority and transcript paths;
        # its matching public lifecycle record supplies the latest observed tool.
        activity = {r['subagent_id']: r for r in list_active_subagents()} if entries else {}
        for row in entries:
            live = activity.get(row['subagent_id'], {})
            row['elapsed'] = max(0, int(now - live.get('started_at', now)))
            row['last_tool'] = live.get('last_tool') or ''
            row['key'] = row['subagent_id']
            row.pop('running_seconds', None)
        processes = procs.process_rows(now)
        signature = json.dumps([entries, processes], sort_keys=True, default=str)
        changed = signature != self._signature
        self._signature = signature
        self.entries = entries
        self.processes = processes
        if self.selected is None:
            roster = self.roster
            self.selected_id = self._key(roster[0]) if roster else None
        return changed

    def invalidate(self):
        from hermes_cli.cli_terminal_mixin import _run_on_app_loop

        app = self.app
        if app is not None:
            # Teardown clears app.loop; don't let it interleave with a worker's
            # invalidate call, which reads the loop more than once.
            _run_on_app_loop(app, app.invalidate)

    def tick(self):
        now = time.monotonic()
        if now - self._last_poll < 1:
            return
        self._last_poll = now
        if self.refresh():
            if self.app is not None:
                self.invalidate()
            else:
                self.cli._invalidate()

    def select(self, delta):
        roster = self.roster
        if roster:
            index = next((i for i, r in enumerate(roster) if self._key(r) == self.selected_id), 0)
            self.selected_id = self._key(roster[(index + delta) % len(roster)])

    def control(self, action, message=None, *, target=None):
        target = target or self.selected_id
        if any(r['key'] == target for r in self.processes):
            if action != 'stop':
                return {'error': 'Background processes cannot be steered; stop them or read the log.'}
            return procs.kill(target)
        from tools.delegate_tool_registry import _handle_control_action
        return json.loads(_handle_control_action(action, target, message, getattr(self.cli, 'agent', None)))

    def _counts(self):
        """Collapsed-heading count fragment: ``2 live``, ``1 proc``, or ``2 live · 3 procs``."""
        parts = []
        if self.entries:
            parts.append(f'{len(self.entries)} live')
        if self.processes:
            running = sum(r['status'] == 'running' for r in self.processes)
            parts.append(f"{running} proc{'s' if running != 1 else ''}" if running else f'{len(self.processes)} done')
        return ' · '.join(parts)

    def _title(self):
        if self.entries and self.processes:
            return 'Live work'
        return 'Subagents' if self.entries else 'Processes'

    def _collapsed_activity(self):
        if self.entries:
            row = self.entries[0]
            return f"last: {row['last_tool']}" if row.get('last_tool') else row.get('status') or 'starting'
        return procs.process_activity(self.processes[0])

    def dock_text(self, *, columns, rows):
        if not self.entries and not self.processes:
            return ''
        if self.collapsed:
            count = self._counts()
            # Keep both controls before spending scarce cells on activity.
            headings = (
                f'{self._title()} · {count} · Ctrl+T expand · F7 restore',
                f'{count} · Ctrl+T expand · F7 restore',
                f'{count} · Ctrl+T · F7',
                count,
            )
            width = max(0, columns - 1)
            heading = next((text for text in headings if get_cwidth(text) <= width), count)
            activity = self._collapsed_activity()
            if get_cwidth(heading + ' · ' + activity) <= width:
                heading += ' · ' + activity
            return _clip(' ' + heading, max(0, columns))
        columns = max(0, columns - 2)
        budget = max(1, min(4, (rows - 10) // 3))
        # Both blocks present: split the row budget so neither hides the other entirely.
        agent_budget = budget if not self.processes else max(1, budget - max(1, budget // 2))
        agent_count = min(len(self.entries), agent_budget)
        lines = []
        if self.entries:
            hidden = len(self.entries) - agent_count
            lines.append(_clip(f' Subagents · {len(self.entries)} live · Ctrl+T expand · F7 collapse', columns))
            for row in self.entries[:agent_count]:
                activity = f"{row['elapsed']}s · " + (f"last: {row['last_tool']}" if row['last_tool'] else row.get('status') or 'starting')
                # Reserve activity even on narrow terminals; task names use the remainder.
                goal_width = max(3, columns - get_cwidth(activity) - 5)
                lines.append(_clip(f" ● {_clip(row.get('goal'), goal_width)} · {activity}", columns))
            if hidden:
                lines.append(_clip(f' +{hidden} more · Ctrl+T all subagents', columns))
        if self.processes:
            proc_count = min(len(self.processes), max(1, budget - agent_count))
            running = sum(r['status'] == 'running' for r in self.processes)
            done = len(self.processes) - running
            summary = ' · '.join(p for p in (f'{running} running' if running else '', f'{done} done' if done else '') if p)
            controls = ' · Ctrl+T expand · F7 collapse' if not self.entries else ''
            lines.append(_clip(f' Processes · {summary}{controls}', columns))
            for row in self.processes[:proc_count]:
                activity = procs.process_activity(row)
                command_width = max(3, columns - get_cwidth(activity) - 5)
                lines.append(_clip(f" {procs.process_glyph(row)} {_clip(row['command'], command_width)} · {activity}", columns))
            if len(self.processes) > proc_count:
                lines.append(_clip(f' +{len(self.processes) - proc_count} more · Ctrl+T all processes', columns))
        return '\n'.join(' ' + line for line in lines)


def read_tail(path):
    if not path:
        return 'Live transcript not available yet.'
    try:
        with open(path, 'rb') as stream:
            stream.seek(0, 2)
            stream.seek(max(0, stream.tell() - 32768))
            text = stream.read(32768).decode('utf-8', errors='replace')
        return ''.join(c for c in text if c.isprintable() or c in '\n\t')
    except OSError:
        return 'Live transcript not available yet.'


def modal_prompt_active(cli):
    return any(getattr(cli, name, None) for name in (
        '_clarify_state', '_approval_state', '_slash_confirm_state', '_sudo_state',
        '_secret_state', '_model_picker_state', '_command_palette_state'))


def build_monitor_application(monitor, **kwargs):
    from prompt_toolkit.application import Application
    from prompt_toolkit.data_structures import Point
    from prompt_toolkit.filters import Condition
    from prompt_toolkit.key_binding import KeyBindings
    from prompt_toolkit.layout import ConditionalContainer, HSplit, Layout, Window
    from prompt_toolkit.layout.controls import FormattedTextControl
    from prompt_toolkit.widgets import TextArea

    state = {'detail': False, 'steering': False, 'confirm': False, 'notice': ''}
    steer = TextArea(height=1, prompt='Steer: ', multiline=False)
    tail = TextArea(read_only=True, scrollbar=True, wrap_lines=True)

    def roster_text():
        size = app.output.get_size()
        rows = []
        for row in monitor.roster:
            selected = monitor._key(row) == monitor.selected_id
            if row.get('kind') == 'process':
                prefix = f"{procs.process_glyph(row)} {procs.process_activity(row)} · "
                activity = ''
                subject = row['command']
            else:
                prefix = f"{row['elapsed']}s · {row.get('status') or 'starting'} · "
                activity = f" · last: {row['last_tool']}" if row.get('last_tool') else ''
                subject = row.get('goal') or row['subagent_id']
            goal_width = max(0, size.columns - 2 - get_cwidth(prefix + activity))
            text = f"{'❯' if selected else ' '} " + _clip(prefix + _clip(subject, goal_width) + activity, max(0, size.columns - 2))
            # Pad selection in terminal cells, not codepoints (task names may be wide).
            text += ' ' * max(0, size.columns - get_cwidth(text))
            rows.append(('class:subagent-dock.selected' if selected else '', text + '\n'))
        return rows or [('', 'No live subagents or background processes. Results arrive in the conversation.')]

    def cursor():
        index = next((i for i, row in enumerate(monitor.roster) if monitor._key(row) == monitor.selected_id), 0)
        return Point(x=0, y=index)

    roster = Window(FormattedTextControl(roster_text, focusable=True, get_cursor_position=cursor))

    def update_tail():
        row = monitor.selected
        if row is None:
            text = 'This entry is no longer live.'
        elif row.get('kind') == 'process':
            text = procs.process_tail(row['id'])
        else:
            text = read_tail(row.get('live_transcript'))
        if text != tail.text:
            following = tail.buffer.cursor_position == len(tail.text)
            position = tail.buffer.cursor_position
            tail.text = text
            tail.buffer.cursor_position = len(text) if following else min(position, len(text))

    def header():
        row = monitor.selected
        title = f"{monitor._title()} · {monitor._counts()}"
        if state['detail'] and row:
            title += f" · {monitor._key(row)} · {row.get('goal') or row.get('command') or ''}"
        return [('class:subagent-dock.heading', _clip(title, app.output.get_size().columns))]

    def footer():
        narrow = app.output.get_size().columns < 60
        process = monitor.selected_process is not None
        if state['confirm']:
            noun = 'process' if process else 'subagent'
            return 'Stop? y yes · Esc cancel' if narrow else f'Stop selected {noun}? y confirm · Esc cancel'
        if state['steering']:
            return 'Enter send · Esc cancel' if narrow else 'Enter queues guidance · Esc cancels (does not interrupt)'
        steer = '' if process else ' s steer'
        if narrow:
            return f'PgUp/Dn ·{steer} x stop · Esc' if state['detail'] else '↑↓ · Enter tail · Ctrl+T close'
        steer = '' if process else ' · s steer'
        return (f'Esc roster · PgUp/PgDn tail{steer} · x stop' if state['detail'] else
                f'↑/↓ select · Enter tail{steer} · x stop · q/Ctrl+T close')

    kb = KeyBindings()
    normal = Condition(lambda: not state['steering'] and not state['confirm'])
    listing = normal & Condition(lambda: not state['detail'])

    @kb.add('up', filter=listing)
    def up(event):
        monitor.select(-1)

    @kb.add('down', filter=listing)
    def down(event):
        monitor.select(1)

    @kb.add('enter', filter=listing)
    def detail(event):
        if monitor.selected:
            state['detail'] = True
            update_tail()
            app.layout.focus(tail)

    @kb.add('s', filter=normal)
    def start_steer(event):
        if monitor.selected and monitor.selected_process is None:
            state['steering'] = True
            state['target'] = monitor.selected_id
            app.layout.focus(steer)

    @kb.add('enter', filter=Condition(lambda: state['steering']))
    def send_steer(event):
        if not steer.text.strip():
            return
        result = monitor.control('steer', steer.text, target=state['target'])
        state['notice'] = result.get('error') or result.get('note') or str(result)
        steer.text = ''
        state['steering'] = False
        app.layout.focus(tail if state['detail'] else roster)

    @kb.add('x', filter=normal)
    def stop(event):
        if monitor.selected:
            state['confirm'] = True
            state['target'] = monitor.selected_id

    @kb.add('y', filter=Condition(lambda: state['confirm']))
    def confirm(event):
        result = monitor.control('stop', target=state['target'])
        state['notice'] = result.get('error') or result.get('note') or str(result)
        state['confirm'] = False

    @kb.add('escape', eager=True)
    def back(event):
        if state['steering'] or state['confirm']:
            state['steering'] = state['confirm'] = False
            app.layout.focus(tail if state['detail'] else roster)
        elif state['detail']:
            state['detail'] = False
            app.layout.focus(roster)
        else:
            app.exit()

    @kb.add('q', filter=normal)
    @kb.add('f6', filter=normal)
    @kb.add('c-t', filter=normal)
    @kb.add('c-c')
    def close(event):
        app.exit()

    layout = Layout(HSplit([
        Window(FormattedTextControl(header), height=1),
        ConditionalContainer(roster, filter=Condition(lambda: not state['detail'])),
        ConditionalContainer(tail, filter=Condition(lambda: state['detail'])),
        ConditionalContainer(steer, filter=Condition(lambda: state['steering'])),
        Window(FormattedTextControl(lambda: _clip(state['notice'], app.output.get_size().columns)), height=1),
        Window(FormattedTextControl(footer), height=1),
    ], style='class:subagent-dock'), focused_element=roster)
    def before_render(app):
        # Prompts arrive on worker threads; exit on the UI loop, including the
        # first frame if a prompt won the race with in_terminal() acquisition.
        if modal_prompt_active(monitor.cli) and not app.is_done:
            app.exit()
        elif state['detail']:
            update_tail()

    from prompt_toolkit.styles import Style
    from hermes_cli.skin_engine import get_prompt_toolkit_style_overrides
    kwargs.setdefault('style', Style.from_dict(get_prompt_toolkit_style_overrides()))
    app = Application(layout=layout, key_bindings=kb, full_screen=True, mouse_support=False,
                      before_render=before_render, **kwargs)
    return app


def open_monitor(cli):
    import asyncio
    from prompt_toolkit.application import in_terminal
    monitor = getattr(cli, '_subagent_monitor', None)
    if monitor is None or monitor.opening:
        return
    monitor.opening = True

    async def run():
        try:
            async with in_terminal():
                monitor.refresh()
                monitor.app = build_monitor_application(monitor)
                await monitor.app.run_async()
        finally:
            monitor.app = None
            monitor.opening = False
            cli._invalidate()

    asyncio.get_running_loop().create_task(run())


def toggle_dock(cli):
    monitor = getattr(cli, '_subagent_monitor', None)
    if monitor is not None:
        monitor.collapsed = not monitor.collapsed
        cli._invalidate()


def install_dock(cli):
    from prompt_toolkit.application import get_app
    from prompt_toolkit.layout import ConditionalContainer, Window
    from prompt_toolkit.layout.controls import FormattedTextControl
    from prompt_toolkit.filters import Condition
    monitor = SubagentMonitor(cli)
    cli._subagent_monitor = monitor
    monitor.refresh()

    def text():
        size = get_app().output.get_size()
        lines = monitor.dock_text(columns=size.columns, rows=size.rows).splitlines()
        return [('class:subagent-dock.heading' if i == 0 else '',
                 line + ('\n' if i < len(lines) - 1 else ''))
                for i, line in enumerate(lines)]

    cli._subagent_dock_widget = ConditionalContainer(
        Window(FormattedTextControl(text), wrap_lines=False, dont_extend_height=True,
               style='class:subagent-dock'),
        filter=Condition(lambda: bool(monitor.entries or monitor.processes) and not modal_prompt_active(cli)),
    )
