#!/usr/bin/env python3
"""
GENESIS COMMAND ROUTER
=======================
Unified command routing and dispatch system.

Features:
    - Command registration with handlers
    - Argument parsing
    - Middleware support
    - Command groups
    - Help generation
    - Async command support

Usage:
    router = CommandRouter()

    @router.command("status")
    def status_cmd(args):
        return {"status": "healthy"}

    result = router.execute("status")
"""

import argparse
import asyncio
import inspect
import json
import re
import shlex
import time
from dataclasses import dataclass, field
from datetime import datetime
from functools import wraps
from pathlib import Path
from typing import Dict, List, Any, Optional, Callable, Union, Tuple, Type
from enum import Enum


class CommandError(Exception):
    """Command execution error."""
    pass


class ArgumentError(Exception):
    """Argument parsing error."""
    pass


@dataclass
class CommandArg:
    """Definition of a command argument."""
    name: str
    arg_type: Type = str
    required: bool = False
    default: Any = None
    help: str = ""
    choices: List[Any] = field(default_factory=list)
    positional: bool = False


@dataclass
class CommandResult:
    """Result of command execution."""
    success: bool
    output: Any
    duration_ms: float
    command: str
    error: Optional[str] = None

    def to_dict(self) -> Dict:
        return {
            "success": self.success,
            "output": self.output,
            "duration_ms": self.duration_ms,
            "command": self.command,
            "error": self.error
        }


@dataclass
class CommandDef:
    """Definition of a command."""
    name: str
    handler: Callable
    description: str = ""
    args: List[CommandArg] = field(default_factory=list)
    group: Optional[str] = None
    aliases: List[str] = field(default_factory=list)
    is_async: bool = False
    hidden: bool = False


class Middleware:
    """Base class for command middleware."""

    def before(self, command: str, args: Dict) -> Tuple[str, Dict]:
        """Called before command execution."""
        return command, args

    def after(self, command: str, result: CommandResult) -> CommandResult:
        """Called after command execution."""
        return result


class LoggingMiddleware(Middleware):
    """Middleware that logs command execution."""

    def __init__(self, logger: Callable[[str], None] = print):
        self.logger = logger

    def before(self, command: str, args: Dict) -> Tuple[str, Dict]:
        self.logger(f"Executing: {command} {args}")
        return command, args

    def after(self, command: str, result: CommandResult) -> CommandResult:
        status = "OK" if result.success else "FAILED"
        self.logger(f"Completed: {command} [{status}] ({result.duration_ms:.2f}ms)")
        return result


class TimingMiddleware(Middleware):
    """Middleware that tracks execution timing."""

    def __init__(self):
        self.timings: List[Tuple[str, float]] = []

    def after(self, command: str, result: CommandResult) -> CommandResult:
        self.timings.append((command, result.duration_ms))
        return result

    def get_stats(self) -> Dict:
        if not self.timings:
            return {}
        durations = [t[1] for t in self.timings]
        return {
            "total_commands": len(self.timings),
            "total_time_ms": sum(durations),
            "avg_time_ms": sum(durations) / len(durations),
            "max_time_ms": max(durations),
            "min_time_ms": min(durations)
        }


class RateLimitMiddleware(Middleware):
    """Middleware that rate limits command execution."""

    def __init__(self, max_per_minute: int = 60):
        self.max_per_minute = max_per_minute
        self.executions: List[float] = []

    def before(self, command: str, args: Dict) -> Tuple[str, Dict]:
        now = time.time()

        # Remove old executions
        self.executions = [t for t in self.executions if now - t < 60]

        if len(self.executions) >= self.max_per_minute:
            raise CommandError(f"Rate limit exceeded ({self.max_per_minute}/minute)")

        self.executions.append(now)
        return command, args


class CommandRouter:
    """
    Routes and executes commands.
    """

    def __init__(self):
        self.commands: Dict[str, CommandDef] = {}
        self.aliases: Dict[str, str] = {}
        self.groups: Dict[str, List[str]] = {}
        self.middleware: List[Middleware] = []
        self._default_handler: Optional[Callable] = None

    def command(
        self,
        name: str,
        description: str = "",
        args: List[CommandArg] = None,
        group: str = None,
        aliases: List[str] = None,
        hidden: bool = False
    ):
        """Decorator to register a command."""
        def decorator(func):
            is_async = asyncio.iscoroutinefunction(func)

            cmd_def = CommandDef(
                name=name,
                handler=func,
                description=description or func.__doc__ or "",
                args=args or [],
                group=group,
                aliases=aliases or [],
                is_async=is_async,
                hidden=hidden
            )

            self.register(cmd_def)
            return func

        return decorator

    def register(self, cmd_def: CommandDef):
        """Register a command definition."""
        self.commands[cmd_def.name] = cmd_def

        # Register aliases
        for alias in cmd_def.aliases:
            self.aliases[alias] = cmd_def.name

        # Add to group
        if cmd_def.group:
            if cmd_def.group not in self.groups:
                self.groups[cmd_def.group] = []
            self.groups[cmd_def.group].append(cmd_def.name)

    def add_middleware(self, middleware: Middleware):
        """Add middleware to the pipeline."""
        self.middleware.append(middleware)

    def set_default(self, handler: Callable):
        """Set default handler for unknown commands."""
        self._default_handler = handler

    def parse_args(self, cmd_def: CommandDef, arg_string: str) -> Dict[str, Any]:
        """Parse command arguments."""
        if not arg_string:
            return self._apply_defaults(cmd_def, {})

        try:
            parts = shlex.split(arg_string)
        except ValueError as e:
            raise ArgumentError(f"Invalid argument format: {e}")

        parsed = {}
        positional_idx = 0

        # Get positional args
        positional_args = [a for a in cmd_def.args if a.positional]

        i = 0
        while i < len(parts):
            part = parts[i]

            if part.startswith('--'):
                # Named argument
                name = part[2:]

                # Find arg definition
                arg_def = next((a for a in cmd_def.args if a.name == name), None)
                if not arg_def:
                    raise ArgumentError(f"Unknown argument: {name}")

                # Get value
                if arg_def.arg_type == bool:
                    parsed[name] = True
                elif i + 1 < len(parts):
                    i += 1
                    parsed[name] = self._convert_value(parts[i], arg_def.arg_type)
                else:
                    raise ArgumentError(f"Missing value for: {name}")

            elif part.startswith('-'):
                # Short argument (not implemented, skip)
                pass

            else:
                # Positional argument
                if positional_idx < len(positional_args):
                    arg_def = positional_args[positional_idx]
                    parsed[arg_def.name] = self._convert_value(part, arg_def.arg_type)
                    positional_idx += 1
                else:
                    raise ArgumentError(f"Unexpected positional argument: {part}")

            i += 1

        return self._apply_defaults(cmd_def, parsed)

    def _apply_defaults(self, cmd_def: CommandDef, parsed: Dict) -> Dict:
        """Apply default values for missing arguments."""
        for arg_def in cmd_def.args:
            if arg_def.name not in parsed:
                if arg_def.required:
                    raise ArgumentError(f"Missing required argument: {arg_def.name}")
                if arg_def.default is not None:
                    parsed[arg_def.name] = arg_def.default

        return parsed

    def _convert_value(self, value: str, target_type: Type) -> Any:
        """Convert string value to target type."""
        if target_type == bool:
            return value.lower() in ('true', '1', 'yes', 'on')
        elif target_type == int:
            return int(value)
        elif target_type == float:
            return float(value)
        elif target_type == list:
            return value.split(',')
        else:
            return value

    def execute(self, command_line: str) -> CommandResult:
        """Execute a command."""
        start_time = time.time()

        # Parse command and args
        parts = command_line.strip().split(' ', 1)
        cmd_name = parts[0]
        arg_string = parts[1] if len(parts) > 1 else ""

        # Resolve alias
        if cmd_name in self.aliases:
            cmd_name = self.aliases[cmd_name]

        # Find command
        cmd_def = self.commands.get(cmd_name)
        if not cmd_def:
            if self._default_handler:
                try:
                    output = self._default_handler(command_line)
                    return CommandResult(
                        success=True,
                        output=output,
                        duration_ms=(time.time() - start_time) * 1000,
                        command=command_line
                    )
                except Exception as e:
                    return CommandResult(
                        success=False,
                        output=None,
                        duration_ms=(time.time() - start_time) * 1000,
                        command=command_line,
                        error=str(e)
                    )
            return CommandResult(
                success=False,
                output=None,
                duration_ms=(time.time() - start_time) * 1000,
                command=command_line,
                error=f"Unknown command: {cmd_name}"
            )

        # Parse arguments
        try:
            args = self.parse_args(cmd_def, arg_string)
        except ArgumentError as e:
            return CommandResult(
                success=False,
                output=None,
                duration_ms=(time.time() - start_time) * 1000,
                command=command_line,
                error=str(e)
            )

        # Run middleware before
        for mw in self.middleware:
            try:
                cmd_name, args = mw.before(cmd_name, args)
            except Exception as e:
                return CommandResult(
                    success=False,
                    output=None,
                    duration_ms=(time.time() - start_time) * 1000,
                    command=command_line,
                    error=str(e)
                )

        # Execute command
        try:
            if cmd_def.is_async:
                output = asyncio.run(cmd_def.handler(**args))
            else:
                output = cmd_def.handler(**args)

            result = CommandResult(
                success=True,
                output=output,
                duration_ms=(time.time() - start_time) * 1000,
                command=command_line
            )

        except Exception as e:
            result = CommandResult(
                success=False,
                output=None,
                duration_ms=(time.time() - start_time) * 1000,
                command=command_line,
                error=str(e)
            )

        # Run middleware after
        for mw in self.middleware:
            try:
                result = mw.after(cmd_name, result)
            except Exception:
                pass

        return result

    async def execute_async(self, command_line: str) -> CommandResult:
        """Execute a command asynchronously."""
        # Similar to execute but supports true async
        return self.execute(command_line)

    def generate_help(self, command: str = None) -> str:
        """Generate help text."""
        if command:
            cmd_def = self.commands.get(command)
            if not cmd_def:
                return f"Unknown command: {command}"

            lines = [
                f"Command: {cmd_def.name}",
                "",
                cmd_def.description or "No description available.",
                ""
            ]

            if cmd_def.aliases:
                lines.append(f"Aliases: {', '.join(cmd_def.aliases)}")
                lines.append("")

            if cmd_def.args:
                lines.append("Arguments:")
                for arg in cmd_def.args:
                    req = "[Required]" if arg.required else "[Optional]"
                    default = f" (default: {arg.default})" if arg.default is not None else ""
                    lines.append(f"  --{arg.name} ({arg.arg_type.__name__}) {req}{default}")
                    if arg.help:
                        lines.append(f"      {arg.help}")
                    if arg.choices:
                        lines.append(f"      Choices: {', '.join(map(str, arg.choices))}")

            return "\n".join(lines)

        # General help
        lines = [
            "Available Commands:",
            "=" * 40,
            ""
        ]

        # Group commands
        grouped: Dict[str, List[CommandDef]] = {"_ungrouped": []}
        for name, cmd in self.commands.items():
            if cmd.hidden:
                continue
            if cmd.group:
                if cmd.group not in grouped:
                    grouped[cmd.group] = []
                grouped[cmd.group].append(cmd)
            else:
                grouped["_ungrouped"].append(cmd)

        # Print ungrouped first
        for cmd in sorted(grouped.pop("_ungrouped", []), key=lambda c: c.name):
            aliases = f" ({', '.join(cmd.aliases)})" if cmd.aliases else ""
            desc = cmd.description.split('\n')[0] if cmd.description else "No description"
            lines.append(f"  {cmd.name}{aliases}")
            lines.append(f"      {desc}")

        # Print grouped
        for group_name, cmds in sorted(grouped.items()):
            lines.extend(["", f"[{group_name}]", ""])
            for cmd in sorted(cmds, key=lambda c: c.name):
                aliases = f" ({', '.join(cmd.aliases)})" if cmd.aliases else ""
                desc = cmd.description.split('\n')[0] if cmd.description else "No description"
                lines.append(f"  {cmd.name}{aliases}")
                lines.append(f"      {desc}")

        return "\n".join(lines)

    def get_completions(self, partial: str) -> List[str]:
        """Get command completions."""
        return [
            name for name in list(self.commands.keys()) + list(self.aliases.keys())
            if name.startswith(partial)
        ]


# Global router instance
_router: Optional[CommandRouter] = None


def get_router() -> CommandRouter:
    """Get global command router."""
    global _router
    if _router is None:
        _router = CommandRouter()
    return _router


def main():
    """CLI and demo for command router."""
    router = CommandRouter()
    router.add_middleware(LoggingMiddleware())
    router.add_middleware(TimingMiddleware())

    # Register some demo commands
    @router.command("status", description="Get system status", group="system")
    def status_cmd():
        return {"status": "healthy", "timestamp": datetime.now().isoformat()}

    @router.command(
        "echo",
        description="Echo a message",
        args=[
            CommandArg("message", str, required=True, positional=True, help="Message to echo"),
            CommandArg("count", int, default=1, help="Number of times to repeat")
        ]
    )
    def echo_cmd(message: str, count: int = 1):
        return {"echoed": message * count}

    @router.command("add", description="Add two numbers", aliases=["sum"])
    def add_cmd(a: int = 0, b: int = 0):
        return {"result": a + b}

    @router.command("help", description="Show help")
    def help_cmd(command: str = None):
        return router.generate_help(command)

    print("Command Router Demo")
    print("=" * 40)

    # Execute some commands
    commands = [
        "status",
        'echo "Hello World" --count 3',
        "add --a 10 --b 20",
        "sum --a 5 --b 15",
        "help",
        "unknown_cmd"
    ]

    for cmd in commands:
        print(f"\n> {cmd}")
        result = router.execute(cmd)
        if result.success:
            print(f"  Result: {json.dumps(result.output, indent=2, default=str)}")
        else:
            print(f"  Error: {result.error}")

    # Get timing stats
    timing_mw = router.middleware[1]
    print(f"\nTiming stats: {json.dumps(timing_mw.get_stats(), indent=2)}")


if __name__ == "__main__":
    main()
