import code
import os
import sys
from importlib import import_module
from pathlib import Path
from typing import Any, Callable, Iterable, List, Optional, TYPE_CHECKING

import click

try:
    from dotenv import load_dotenv
except ImportError:
    pass


from .__about__ import __version__
from .helpers import get_debug_flag

if TYPE_CHECKING:
    from .app import Quart  # noqa: F401


class NoAppException(click.UsageError):

    def __init__(self) -> None:
        super().__init__(
            'Could not locate a Quart application as the QUART_APP environment '
            'variable has either not been set or provided or does not point to '
            'a valid application.\n'
            'Please set it to module_name:app_name or module_name:factory_function()\n'
            'Note `quart` is not a valid module_name.'
        )


class ScriptInfo:

    def __init__(
            self,
            app_import_path: Optional[str]=None,
            create_app: Optional[Callable]=None,
    ) -> None:
        self.load_dotenv_if_exists()
        self.app_import_path = app_import_path or os.environ.get('QUART_APP')
        self.create_app = create_app
        self.data: dict = {}
        self._app: Optional['Quart'] = None

    def load_app(self) -> 'Quart':
        if self._app is None:
            if self.create_app is not None:
                self._app = self.create_app()
            else:
                try:
                    module_name, app_name = self.app_import_path.split(':', 1)
                except ValueError:
                    module_name, app_name = self.app_import_path, 'app'
                except AttributeError:
                    raise NoAppException()

                module_path = Path(module_name).resolve()
                sys.path.insert(0, str(module_path.parent))
                if module_path.is_file():
                    import_name = module_path.with_suffix('').name
                else:
                    import_name = module_path.name
                try:
                    module = import_module(import_name)
                except ModuleNotFoundError as error:
                    if error.name == import_name:
                        raise NoAppException()
                    else:
                        raise

                try:
                    self._app = eval(app_name, vars(module))
                except NameError:
                    raise NoAppException()

                from .app import Quart
                if not isinstance(self._app, Quart):
                    self._app = None
                    raise NoAppException()

        if self._app is None:
            raise NoAppException()

        self._app.debug = get_debug_flag()

        return self._app

    def load_dotenv_if_exists(self) -> None:
        if os.environ.get('QUART_SKIP_DOTENV') == '1':
            return

        if not Path(".env").is_file() and not Path(".quartenv").is_file():
            return

        try:
            if Path(".env").is_file():
                load_dotenv()
            if Path(".quartenv").is_file():
                load_dotenv(dotenv_path=Path(".") / ".quartenv")
        except NameError:
            print(  # noqa: T001
                "* Tip: There are .env files present. "
                "Do \"pip install python-dotenv\" to use them."
            )


pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True)


class AppGroup(click.Group):

    def group(self, *args: Any, **kwargs: Any) -> Any:
        kwargs.setdefault('cls', AppGroup)
        return super().group(self, *args, **kwargs)


def get_version(ctx: Any, param: Any, value: Any) -> None:
    if not value or ctx.resilient_parsing:
        return
    message = f"Quart {__version__}"
    click.echo(message, color=ctx.color)
    ctx.exit()


version_option = click.Option(
    ['--version'], help='Show the Quart version', expose_value=False,
    callback=get_version, is_flag=True, is_eager=True,
)


class QuartGroup(AppGroup):

    def __init__(
            self,
            add_default_commands: bool=True,
            create_app: Optional[Callable]=None,
            add_version_option: bool=True,
            *,
            params: Optional[List]=None,
            **kwargs: Any,
    ) -> None:
        params = params or []
        if add_version_option:
            params.append(version_option)
        super().__init__(params=params, **kwargs)
        self.create_app = create_app

        if add_default_commands:
            self.add_command(run_command)
            self.add_command(shell_command)

    def get_command(self, ctx: click.Context, name: str) -> click.Command:
        """Return the relevant command given the context and name.

        .. warning::

            This differs substaintially from Flask in that it allows
            for the inbuilt commands to be overridden.
        """
        info = ctx.ensure_object(ScriptInfo)
        command = None
        try:
            command = info.load_app().cli.get_command(ctx, name)
        except NoAppException:
            pass
        if command is None:
            command = super().get_command(ctx, name)
        return command

    def list_commands(self, ctx: click.Context) -> Iterable[str]:
        commands = set(click.Group.list_commands(self, ctx))
        info = ctx.ensure_object(ScriptInfo)
        commands.update(info.load_app().cli.list_commands(ctx))
        return commands

    def main(self, *args: Any, **kwargs: Any) -> Any:
        kwargs.setdefault('obj', ScriptInfo(create_app=self.create_app))
        return super().main(*args, **kwargs)


@click.command('run', short_help='Start and run a development server.')
@click.option('--host', '-h', default='127.0.0.1', help='The interface to serve on.')
@click.option('--port', '-p', default=5000, help='The port to serve on.')
@pass_script_info
def run_command(info: ScriptInfo, host: str, port: int) -> None:
    app = info.load_app()
    app.run(debug=True, host=host, port=port, use_reloader=True)


@click.command('shell', short_help='Open a shell within the app context.')
@pass_script_info
def shell_command(info: ScriptInfo) -> None:
    app = info.load_app()
    context = {}
    context.update(app.make_shell_context())

    banner = f"Python {sys.version} on {sys.platform} running {app.import_name}"
    code.interact(banner=banner, local=context)


cli = QuartGroup(
    help="""\
Utility functions for Quart applications.

This will load the app defined in the QUART_APP environment
variable. The QUART_APP variable follows the Gunicorn standard of
`module_name:application_name` e.g. `hello:app`.

\b
{prefix}{cmd} QUART_APP=hello:app
{prefix}{cmd} QUART_DEBUG=1
{prefix}quart run
    """.format(
        cmd='export' if os.name == 'posix' else 'set',
        prefix='$ ' if os.name == 'posix' else '> ',
    ),
)


def main(as_module: bool=False) -> None:
    args = sys.argv[1:]

    if as_module:
        name = 'python -m quart'
        sys.argv = ['-m', 'quart'] + args
    else:
        name = None

    cli.main(args=args, prog_name=name)


if __name__ == '__main__':
    main(as_module=True)
