"""Common code for command line interface tool for the Jac language."""

from __future__ import annotations

import argparse
import inspect
import os
import re
import sys
from dataclasses import fields as dataclass_fields
from enum import IntEnum
from typing import Callable, Dict, Optional

from jaclang.settings import Settings as JacSettings


class CommandPriority(IntEnum):
    """Priority levels for command registration.

    Higher values take precedence when multiple commands with the same name are registered.
    This allows plugins to override core commands in a controlled manner.
    """

    CORE = 100  # Core jaclang commands (lowest priority, can be overridden)
    PLUGIN = 200  # Plugin-provided commands (override core)
    USER = 300  # User-defined commands (highest priority, override everything)


class Command:
    """Represents a command in the command line interface."""

    func: Callable
    sig: inspect.Signature
    priority: CommandPriority
    source: str  # Source plugin/module name

    def __init__(
        self,
        func: Callable,
        priority: CommandPriority = CommandPriority.CORE,
        source: str = "core",
    ) -> None:
        """Initialize a Command instance."""
        self.func = func
        self.sig = inspect.signature(func)
        self.priority = priority
        self.source = source

    def call(self, *args: list, **kwargs: dict) -> str:
        """Call the associated function with the specified arguments and keyword arguments."""
        return self.func(*args, **kwargs)


def extract_param_descriptions(docstring: str) -> Dict[str, str]:
    """Extract parameter descriptions from a function's docstring.

    Args:
        docstring: The function's docstring

    Returns:
        A dictionary mapping parameter names to their descriptions
    """
    param_descriptions: dict[str, str] = {}

    # Check if the docstring has an Args section
    args_match = re.search(r"Args:(.*?)(?:\n\n|\Z)", docstring, re.DOTALL)
    if not args_match:
        return param_descriptions

    args_section = args_match.group(1)

    # Extract parameter descriptions from the Args section
    # Looking for patterns like "param_name: Description" or "param_name (type): Description"
    current_param = None
    current_desc = []

    for line in args_section.strip().split("\n"):
        line = line.strip()
        if not line:
            continue

        # Match parameter name at the beginning of the line
        param_match = re.match(r"\s*([a-zA-Z0-9_]+)(?:\s*\([^)]*\))?:\s*(.*)", line)
        if param_match:
            # Save the previous parameter if there was one
            if current_param and current_desc:
                param_descriptions[current_param] = " ".join(current_desc)
                current_desc = []

            # Start a new parameter
            current_param = param_match.group(1)
            if param_match.group(2):
                current_desc.append(param_match.group(2))
        elif current_param and line:
            # Continue the description for the current parameter
            current_desc.append(line)

    # Save the last parameter
    if current_param and current_desc:
        param_descriptions[current_param] = " ".join(current_desc)

    return param_descriptions


class CommandRegistry:
    """Registry for managing commands in the command line interface."""

    registry: dict[str, Command]
    pending_commands: dict[str, list[Command]]  # Commands waiting to be bound
    sub_parsers: argparse._SubParsersAction
    parser: argparse.ArgumentParser
    args: argparse.Namespace
    _finalized: bool  # Whether command registration has been finalized

    def __init__(self) -> None:
        """Initialize a CommandRegistry instance."""
        self.registry = {}
        self.pending_commands = {}
        self._finalized = False
        self.parser = argparse.ArgumentParser(
            prog="jac",
            description="Jac Programming Language CLI - A tool for working with Jac programs",
            epilog="For more information, visit: https://github.com/Jaseci-Labs/jaseci",
            formatter_class=argparse.RawDescriptionHelpFormatter,
        )
        self.parser.add_argument(
            "-V",
            "--version",
            action="store_true",
            help="Show the Jac version and installation path",
        )
        self.sub_parsers = self.parser.add_subparsers(
            title="commands",
            dest="command",
            description="The following commands are available:",
            metavar="COMMAND",
        )
        self.args = argparse.Namespace()

        # Global settings overrides
        settings_group = self.parser.add_argument_group(
            "settings",
            "Override Jac settings (from config/env). Provide only flags you want to change.",
        )
        for fld in dataclass_fields(JacSettings):
            name = fld.name
            opt = f"--{name.replace('_', '-')}"
            if fld.type is bool:
                # Tri-state: default None so unspecified won't override
                settings_group.add_argument(
                    opt,
                    dest=name,
                    action="store_true",
                    default=None,
                    help=f"bool - Override setting '{name}' to true",
                )
                settings_group.add_argument(
                    f"--no-{name.replace('_', '-')}",
                    dest=name,
                    action="store_false",
                    default=None,
                    help=f"bool - Override setting '{name}' to false",
                )
            elif fld.type is int:
                settings_group.add_argument(
                    opt,
                    dest=name,
                    type=int,
                    default=None,
                    help=f"int - Override setting '{name}'",
                )
            else:
                settings_group.add_argument(
                    opt,
                    dest=name,
                    type=str,
                    default=None,
                    help=f"str - Override setting '{name}'",
                )

    def register(
        self,
        func: Callable | None = None,
        *,
        priority: CommandPriority = CommandPriority.CORE,
        source: str = "core",
    ) -> Callable:
        """Register a command in the registry.

        This method supports both decorator syntax with and without arguments:
            @cmd_registry.register
            def my_cmd(): ...

            @cmd_registry.register(priority=CommandPriority.PLUGIN, source="my-plugin")
            def my_cmd(): ...

        Args:
            func: The command function to register
            priority: Priority level for conflict resolution
            source: Source plugin/module name for introspection

        Returns:
            The original function (for decorator usage)
        """

        def _register(f: Callable) -> Callable:
            """Inner registration function."""
            name = f.__name__
            cmd = Command(f, priority=priority, source=source)

            # Add to pending commands for priority resolution
            if name not in self.pending_commands:
                self.pending_commands[name] = []
            self.pending_commands[name].append(cmd)

            # If already finalized, bind immediately (late registration)
            if self._finalized:
                self._resolve_and_bind_command(name)

            return f

        # Support both @register and @register(...) syntax
        if func is not None:
            return _register(func)
        return _register

    def _bind_command_to_argparse(self, name: str, cmd: Command) -> None:
        """Bind a command to argparse subparser."""
        func = cmd.func
        # Extract the first paragraph from the docstring for brief description
        doc = func.__doc__ or ""
        brief_desc = doc.split("\n\n")[0].strip()

        # Use the full docstring for the detailed description
        cmd_parser: argparse.ArgumentParser = self.sub_parsers.add_parser(
            name,
            description=func.__doc__,
            help=brief_desc,
            formatter_class=argparse.RawDescriptionHelpFormatter,
        )
        first = True
        for param_name, param in cmd.sig.parameters.items():
            # Get parameter type name
            type_name = (
                param.annotation.__name__
                if hasattr(param.annotation, "__name__")
                else str(param.annotation)
            )

            # Extract parameter descriptions from the docstring
            docstring_param_descriptions = extract_param_descriptions(doc)

            # Use docstring description if available, otherwise fallback to predefined or just type
            if param_name in docstring_param_descriptions:
                arg_msg = f"{type_name} - {docstring_param_descriptions[param_name]}"
            else:
                arg_msg = f"{type_name}"
            # shorthand is first character by default,
            # If already taken, use the first 2 characters
            shorthand = param_name[:1]
            if f"-{shorthand}" in cmd_parser._option_string_actions:
                shorthand = param_name[:2]
            if param_name == "args":
                cmd_parser.add_argument("args", nargs=argparse.REMAINDER, help=arg_msg)
            elif param_name == "filepath":
                first = False
                cmd_parser.add_argument(
                    f"{param_name}",
                    type=(
                        eval(param.annotation)
                        if isinstance(param.annotation, str)
                        else param.annotation
                    ),
                    help=arg_msg,
                    nargs="?",
                )
            elif param.default is param.empty:
                if first:
                    first = False
                    cmd_parser.add_argument(
                        f"{param_name}",
                        type=(
                            eval(param.annotation)
                            if isinstance(param.annotation, str)
                            else param.annotation
                        ),
                        help=arg_msg,
                    )
                else:
                    cmd_parser.add_argument(
                        f"-{shorthand}",
                        f"--{param_name}",
                        required=True,
                        type=(
                            eval(param.annotation)
                            if isinstance(param.annotation, str)
                            else param.annotation
                        ),
                        help=arg_msg,
                    )
            elif first:
                arg_msg += f", default: {param.default}"
                first = False
                cmd_parser.add_argument(
                    f"{param_name}",
                    default=param.default,
                    type=eval(param.annotation),
                    help=arg_msg,
                )
            else:
                arg_msg += f", default: {param.default}"
                if param.annotation == bool:
                    cmd_parser.add_argument(
                        f"-{shorthand}",
                        f"--{param_name}",
                        default=param.default,
                        action="store_true",
                        help=arg_msg,
                    )
                    cmd_parser.add_argument(
                        f"-n{shorthand}",
                        f"--no-{param_name}",
                        dest=param_name,
                        action="store_false",
                        help=f"Compliment of {arg_msg}",
                    )
                else:
                    cmd_parser.add_argument(
                        f"-{shorthand}",
                        f"--{param_name}",
                        default=param.default,
                        help=arg_msg,
                        type=(
                            eval(param.annotation)
                            if isinstance(param.annotation, str)
                            else param.annotation
                        ),
                    )

    def _resolve_and_bind_command(self, name: str) -> None:
        """Resolve command conflicts by priority and bind to argparse."""
        if name not in self.pending_commands:
            return

        commands = self.pending_commands[name]
        if not commands:
            return

        # Sort by priority (highest first)
        commands.sort(key=lambda c: c.priority, reverse=True)

        # Winner is the highest priority command
        winner = commands[0]

        # Warn about conflicts if multiple commands with different priorities
        # Only warn if JAC_CLI_VERBOSE environment variable is set
        if len(commands) > 1 and os.getenv("JAC_CLI_VERBOSE"):
            conflicts = [f"{c.source} (priority={c.priority})" for c in commands[1:]]
            print(
                f"Warning: Command '{name}' registered by multiple sources. "
                f"Using {winner.source} (priority={winner.priority}). "
                f"Overriding: {', '.join(conflicts)}",
                file=sys.stderr,
            )

        # Register the winner
        self.registry[name] = winner
        self._bind_command_to_argparse(name, winner)

    def finalize(self) -> None:
        """Finalize command registration by resolving conflicts and binding to argparse.

        This should be called after all plugins have had a chance to register commands.
        """
        if self._finalized:
            return

        # Resolve all pending commands
        for name in list(self.pending_commands.keys()):
            self._resolve_and_bind_command(name)

        self._finalized = True
        self.pending_commands.clear()

    def get(self, name: str) -> Optional[Command]:
        """Get the Command instance for a given command name."""
        return self.registry.get(name)

    def get_all_commands(self) -> dict:
        """Get all registered commands along with their details."""
        all_commands = {}
        for name, comd in self.registry.items():
            doc = comd.func.__doc__ or "No help available."
            args = comd.sig.parameters
            all_commands[name] = (doc, args)
        return all_commands

    def has_command(self, name: str) -> bool:
        """Check if a command is already registered."""
        return name in self.registry

    def list_commands(self) -> dict[str, dict[str, CommandPriority | str]]:
        """List all registered commands with metadata.

        Returns:
            Dictionary mapping command names to metadata including source and priority
        """
        return {
            name: {
                "source": cmd.source,
                "priority": cmd.priority,
                "priority_name": cmd.priority.name,
                "doc": cmd.func.__doc__ or "No documentation",
            }
            for name, cmd in self.registry.items()
        }


cmd_registry = CommandRegistry()


# Shell mode removed; interactive cmd-based shell is no longer supported.
