from __future__ import annotations

import argparse
import inspect
import os
import sys
from typing import Any, Callable, Dict, Optional, Union, get_args, get_origin, get_type_hints

from .session import session as global_session, Session


CommandFunc = Callable[..., Any]


class App:
    """Speedi application and command registry.

    Usage:
        app = App()

        @app.command()
        def greet(name: str, age: int, loud: bool = False):
            ...

        if __name__ == "__main__":
            app.run()
    """

    def __init__(self, name: str = "speedi", description: Optional[str] = None) -> None:
        self.name = name
        self.description = description or "Speedi CLI"
        self._commands: Dict[str, CommandFunc] = {}
        self.session: Session = global_session

    def command(self, name: Optional[str] = None) -> Callable[[CommandFunc], CommandFunc]:
        """Decorator to register a function as a command."""

        def decorator(func: CommandFunc) -> CommandFunc:
            cmd_name = name or func.__name__
            if cmd_name in self._commands:
                raise ValueError(f"Command '{cmd_name}' already registered")
            self._commands[cmd_name] = func
            return func

        return decorator

    # -----------------
    # Argparse building
    # -----------------
    def _build_parser(self) -> argparse.ArgumentParser:
        parser = argparse.ArgumentParser(prog=self.name, description=self.description)
        parser.add_argument(
            "--persist",
            action="store_true",
            help="Persist session to disk after command (or set SPEEDI_PERSIST=1)",
        )
        subparsers = parser.add_subparsers(dest="command", required=True)

        for cmd_name, func in self._commands.items():
            sp = subparsers.add_parser(cmd_name, help=func.__doc__ or "")
            self._add_function_signature_to_parser(sp, func)

        return parser

    def _add_function_signature_to_parser(self, sp: argparse.ArgumentParser, func: CommandFunc) -> None:
        sig = inspect.signature(func)
        hints = get_type_hints(func)
        for name, param in sig.parameters.items():
            ann = hints.get(name, param.annotation)
            has_default = param.default is not inspect._empty
            default = None if not has_default else param.default

            # Determine argparse config
            arg_name = name.replace("_", "-")
            origin = get_origin(ann)
            args = get_args(ann)

            # Bool flags
            if ann is bool or ann == bool or isinstance(default, bool):
                # --flag / --no-flag
                if default is True:
                    sp.add_argument(
                        f"--no-{arg_name}", dest=name, action="store_false", help=f"Disable {arg_name}",
                    )
                else:
                    sp.add_argument(
                        f"--{arg_name}", dest=name, action="store_true", help=f"Enable {arg_name}",
                    )
                continue

            # List[T]
            if origin is list:
                elem_type = args[0] if args else str
                if has_default:
                    # Optional positional list when a default is provided
                    sp.add_argument(name, nargs="*", type=_to_type(elem_type), default=default or [])
                else:
                    # Required one or more
                    sp.add_argument(name, nargs="+", type=_to_type(elem_type))
                continue

            # Optionals -> treat as option
            if origin is Union and type(None) in args:
                # Optional[T] -> --name T
                t = [a for a in args if a is not type(None)][0]
                sp.add_argument(f"--{arg_name}", dest=name, type=_to_type(t), default=default)
                continue

            # Defaulted params -> --name VALUE
            if has_default:
                sp.add_argument(f"--{arg_name}", dest=name, type=_to_type(ann), default=default)
            else:
                # Positional
                sp.add_argument(name, type=_to_type(ann))

    # -------------
    # Run / Invoke
    # -------------
    def run(self, argv: Optional[list[str]] = None) -> None:
        parser = self._build_parser()
        ns = parser.parse_args(argv)

        cmd_name: str = ns.command
        func = self._commands.get(cmd_name)
        if not func:
            raise SystemExit(f"Unknown command: {cmd_name}")

        # Build kwargs from argparse namespace, dropping parser internals
        drop_keys = {"command", "persist", "args"}
        kwargs = {k: v for k, v in vars(ns).items() if k not in drop_keys}

        result = func(**kwargs)
        if inspect.iscoroutine(result):
            # Run async function
            import asyncio

            output = asyncio.run(result)  # noqa: S311
        else:
            output = result

        if output is not None:
            print(output)

        should_persist = bool(getattr(ns, "persist", False) or os.getenv("SPEEDI_PERSIST"))
        if should_persist and self.session.mutated:
            self.session.save()

    def invoke(self, command: str, *args: Any, persist: bool = False, **kwargs: Any) -> Any:
        """Programmatic invocation for tests. kwargs map to --key value for options, bools become flags.
        Positional args map first.
        """
        argv: list[str] = [command]
        # Options from kwargs
        for key, val in kwargs.items():
            k = f"--{key.replace('_', '-')}"
            if isinstance(val, bool):
                if val:
                    argv.append(k)
            else:
                argv.extend([k, str(val)])
        # Positionals
        argv.extend([str(a) for a in args])
        if persist:
            argv.insert(0, "--persist")
        # Capture output by running normally
        self.run(argv)


# -------------
# Helpers
# -------------

def _to_type(ann: Any) -> Callable[[str], Any]:
    origin = get_origin(ann)
    if origin is None:
        # Plain types or empty annotations
        if ann in (inspect._empty, Any, None):
            return str
        if ann is bool:
            return _parse_bool
        if ann in (int, float, str):
            return ann
        return str
    # typing constructs
    if origin is list:
        args = get_args(ann)
        t = args[0] if args else str
        return _to_type(t)
    if origin is Optional:
        args = [a for a in get_args(ann) if a is not type(None)]
        return _to_type(args[0] if args else str)
    # Fallback
    return str


def _parse_bool(v: str) -> bool:
    if isinstance(v, bool):
        return v
    s = v.strip().lower()
    if s in {"1", "true", "t", "yes", "y", "on"}:
        return True
    if s in {"0", "false", "f", "no", "n", "off"}:
        return False
    raise argparse.ArgumentTypeError(f"Invalid boolean value: {v}")
