"""command/reactor.py

Reactor control commands (shutdown, reload, help, etc.)

Created by Thomas Mangin on 2017-07-01.
Copyright (c) 2009-2017 Exa Networks. All rights reserved.
License: 3-clause BSD. (See the COPYRIGHT file)
"""

from __future__ import annotations

import asyncio
import json
from typing import TYPE_CHECKING

from exabgp.version import version as _version

from exabgp.logger import log, lazymsg

if TYPE_CHECKING:
    from exabgp.reactor.api import API
    from exabgp.reactor.loop import Reactor


def register_reactor() -> None:
    pass


def help_command(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    from exabgp.reactor.api.dispatch import get_commands

    commands = get_commands()

    if use_json:
        # Build JSON structure with command metadata
        commands_list = []

        for cmd_name, neighbor_support, options in sorted(commands, key=lambda x: x[0]):
            cmd_info = {
                'command': cmd_name,
                'neighbor_support': neighbor_support,
                'json_support': True,
            }
            if options:
                cmd_info['options'] = options
            commands_list.append(cmd_info)

        help_data = {
            'description': 'Available API commands (v6 format)',
            'peer_filters': ['local-ip', 'local-as', 'peer-as', 'router-id'],
            'commands': commands_list,
        }

        reactor.processes.write(service, json.dumps(help_data))
    else:
        # Text mode output
        lines = []

        for cmd_name, neighbor_support, options in sorted(commands, key=lambda x: x[0]):
            if options:
                opts_str = ' | '.join(str(o) for o in options)
                extended = f'{cmd_name} [ {opts_str} ]'
            else:
                extended = cmd_name
            lines.append('[peer <ip> [filters]] ' + extended if neighbor_support else f'{extended} ')

        reactor.processes.write(service, '')
        reactor.processes.write(service, 'available API commands (v6 format):')
        reactor.processes.write(service, '====================================')
        reactor.processes.write(service, '')
        reactor.processes.write(
            service,
            'filter can be: [local-ip <ip>][local-as <asn>][peer-as <asn>][router-id <router-id>]',
        )
        reactor.processes.write(service, '')
        reactor.processes.write(service, 'commands:')
        reactor.processes.write(service, '---------')
        reactor.processes.write(service, '')
        for line_text in sorted(lines):
            reactor.processes.write(service, line_text)
        reactor.processes.write(service, '')

    reactor.processes.answer_done_sync(service)
    return True


def shutdown(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    reactor.signal.received = reactor.signal.SHUTDOWN
    if use_json:
        reactor.processes.write(service, json.dumps({'status': 'shutdown in progress'}))
    else:
        reactor.processes.write(service, 'shutdown in progress')
    reactor.processes.answer_done_sync(service)
    return True


def reload(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    reactor.signal.received = reactor.signal.RELOAD
    if use_json:
        reactor.processes.write(service, json.dumps({'status': 'reload in progress'}))
    else:
        reactor.processes.write(service, 'reload in progress')
    reactor.processes.answer_done_sync(service)
    return True


def restart(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    reactor.signal.received = reactor.signal.RESTART
    if use_json:
        reactor.processes.write(service, json.dumps({'status': 'restart in progress'}))
    else:
        reactor.processes.write(service, 'restart in progress')
    reactor.processes.answer_done_sync(service)
    return True


def version(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    if use_json:
        reactor.processes.write(service, json.dumps({'version': _version, 'application': 'exabgp'}))
    else:
        reactor.processes.write(service, f'exabgp {_version}')
    reactor.processes.answer_done_sync(service)
    return True


def comment(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    log.debug(lazymsg('api.comment text={text}', text=command.lstrip().lstrip('#').strip()), 'processes')
    reactor.processes.answer_done_sync(service)
    return True


def reset(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    reactor.asynchronous.clear(service)

    if use_json:
        reactor.processes.write(service, json.dumps({'status': 'asynchronous queue cleared'}))
    else:
        reactor.processes.write(service, 'asynchronous queue cleared')

    reactor.processes.answer_done_sync(service)
    return True


def queue_status(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Display write queue status for all API processes.

    Returns queue size (items and bytes) for each process.
    Useful for monitoring backpressure and diagnosing slow API clients.
    """
    stats = reactor.processes.get_queue_stats()

    if use_json:
        reactor.processes.write(service, json.dumps(stats))
    else:
        # Text format: process: N items (M bytes)
        if not stats:
            reactor.processes.write(service, 'no queued messages')
        else:
            lines = []
            for process_name, process_stats in sorted(stats.items()):
                items = process_stats['items']
                bytes_count = process_stats['bytes']
                lines.append(f'{process_name}: {items} items ({bytes_count} bytes)')
            reactor.processes.write(service, '\n'.join(lines))

    reactor.processes.answer_done_sync(service)
    return True


def crash(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    async def callback() -> None:
        raise ValueError('crash test of the API')
        await asyncio.sleep(0)  # This line is unreachable but matches original structure

    # Send acknowledgment before scheduling the crash
    reactor.processes.answer_done_sync(service)
    reactor.asynchronous.schedule(service, command, callback())
    return True


def disable_ack(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Disable ACK responses for this connection (sends 'done' for this command, then disables)"""
    reactor.processes.set_ack(service, False)
    reactor.processes.answer_done_sync(service, force=True)
    return True


def enable_ack(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Re-enable ACK responses for this connection"""
    reactor.processes.set_ack(service, True)
    reactor.processes.answer_done_sync(service)
    return True


def silence_ack(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Disable ACK responses immediately (no 'done' sent for this command)"""
    reactor.processes.set_ack(service, False)
    return True


def enable_sync(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Enable sync mode - wait for routes to be flushed to wire before ACK.

    When sync mode is enabled, announce/withdraw commands will wait until
    the routes have been sent on the wire to the BGP peer before returning
    the ACK response. This allows API processes to know when routes have
    actually been transmitted.
    """
    reactor.processes.set_sync(service, True)
    reactor.processes.answer_done_sync(service)
    return True


def disable_sync(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Disable sync mode - ACK immediately after RIB update (default).

    When sync mode is disabled (default), announce/withdraw commands return
    ACK immediately after the route is added to the RIB, without waiting
    for it to be sent on the wire.
    """
    reactor.processes.set_sync(service, False)
    reactor.processes.answer_done_sync(service)
    return True


def ping(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Lightweight health check - responds with 'pong <UUID>' and active status

    Defaults to JSON output unless 'text' keyword is explicitly used.
    """
    # Parse client UUID and start time if provided
    # Format: "ping <client_uuid> <client_start_time>"
    parts = command.strip().split()
    client_uuid = None
    client_start_time = None

    # Check if 'text' keyword is explicitly used in the command line
    # Default to JSON unless text is explicitly requested
    if 'text' in [p.lower() for p in parts]:
        output_json = False
    else:
        output_json = True

    if len(parts) >= 2:
        client_uuid = parts[0]
        try:
            client_start_time = float(parts[1])
        except ValueError:
            pass

    # Multi-client support: all clients are active
    is_active = True
    if client_uuid and client_start_time is not None:
        import time

        current_time = time.time()
        client_timeout = 15  # seconds - 10s ping interval + 5s grace

        # Clean up stale clients (no ping received within timeout)
        stale_uuids = [
            uuid for uuid, last_ping in reactor.active_clients.items() if current_time - last_ping > client_timeout
        ]
        for uuid in stale_uuids:
            del reactor.active_clients[uuid]

        # Update this client's ping time (all clients are active in multi-client mode)
        reactor.active_clients[client_uuid] = current_time

    if output_json:
        response = {'pong': reactor.daemon_uuid, 'active': is_active}
        reactor.processes.write(service, json.dumps(response))
    else:
        reactor.processes.write(service, f'pong {reactor.daemon_uuid} active={str(is_active).lower()}')
    reactor.processes.answer_done_sync(service)
    return True


def bye(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Handle client disconnect - cleanup client tracking

    Format: "bye <client_uuid>"
    Called by socket server when a client disconnects.
    """
    # Parse client UUID if provided
    parts = command.strip().split()
    client_uuid = parts[0] if parts else None

    # Remove client from active clients tracking
    if client_uuid and client_uuid in reactor.active_clients:
        del reactor.active_clients[client_uuid]

    reactor.processes.answer_done_sync(service)
    return True


def api_version_cmd(
    self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool
) -> bool:
    """Display or set the API version.

    Usage:
        api version       - Show current API version (4=legacy, 6=json-only)
        api version 4     - Set API version to 4 (legacy, supports text/json)
        api version 6     - Set API version to 6 (json-only, default)

    Note: Version changes take effect on next process restart.
    """
    from exabgp.environment import getenv

    parts = command.strip().split()

    # Check if a version number was provided (first part after command prefix stripped)
    if parts:
        version_str = parts[0]
        try:
            new_version = int(version_str)
            if new_version not in (4, 6):
                if use_json:
                    reactor.processes.write(service, json.dumps({'error': 'API version must be 4 or 6'}))
                else:
                    reactor.processes.write(service, 'error: API version must be 4 or 6')
                reactor.processes.answer_error_sync(service)
                return False

            # Set the new version in the environment
            getenv().api.version = new_version

            if use_json:
                reactor.processes.write(
                    service,
                    json.dumps(
                        {
                            'status': 'API version set',
                            'version': new_version,
                            'note': 'effective on next process restart',
                        }
                    ),
                )
            else:
                reactor.processes.write(
                    service, f'API version set to {new_version} (effective on next process restart)'
                )

        except ValueError:
            if use_json:
                reactor.processes.write(service, json.dumps({'error': f'Invalid version: {version_str}'}))
            else:
                reactor.processes.write(service, f'error: invalid version: {version_str}')
            reactor.processes.answer_error_sync(service)
            return False
    else:
        # Just show current version
        current_version = getenv().api.version
        if use_json:
            reactor.processes.write(
                service,
                json.dumps(
                    {
                        'api_version': current_version,
                        'description': 'legacy (text/json)' if current_version == 4 else 'json-only',
                    }
                ),
            )
        else:
            desc = 'legacy (text/json)' if current_version == 4 else 'json-only'
            reactor.processes.write(service, f'API version: {current_version} ({desc})')

    reactor.processes.answer_done_sync(service)
    return True


def status(self: 'API', reactor: 'Reactor', service: str, peers: list[str], command: str, use_json: bool) -> bool:
    """Display daemon status information (UUID, uptime, version, peers)"""
    import os
    import time

    uptime = int(time.time() - reactor.daemon_start_time)
    hours = uptime // 3600
    minutes = (uptime % 3600) // 60
    seconds = uptime % 60

    peers_dict = {}
    for name, peer in reactor._peers.items():
        state = peer.fsm.name()
        peers_dict[name] = state

    if use_json:
        status_info = {
            'version': _version,
            'uuid': reactor.daemon_uuid,
            'pid': os.getpid(),
            'uptime': uptime,
            'start_time': reactor.daemon_start_time,
            'peers': peers_dict,
        }
        reactor.processes.write(service, json.dumps(status_info))
    else:
        lines = [
            'ExaBGP Daemon Status',
            '====================',
            f'Version: {_version}',
            f'UUID: {reactor.daemon_uuid}',
            f'PID: {os.getpid()}',
            f'Uptime: {hours}h {minutes}m {seconds}s',
            f'Peers: {len(peers_dict)}',
        ]

        if peers_dict:
            for name, state in peers_dict.items():
                lines.append(f'  - {name}: {state}')

        for line_text in lines:
            reactor.processes.write(service, line_text)

    reactor.processes.answer_done_sync(service)
    return True
