import os
import pkgutil
import sys
from functools import wraps
from pathlib import Path
from typing import Any, Callable, List, Optional, Tuple, Union
from urllib.parse import quote

from .ctx import _app_ctx_stack, _request_ctx_stack
from .globals import current_app, request, session
from .routing import BuildError
from .signals import message_flashed
from .wrappers import Response

locked_cached_property = property


def get_debug_flag() -> bool:
    """ Reads QUART_DEBUG environment variable to determine whether to run
        the app in debug mode. If unset, and development mode has been
        configured, it will be enabled automatically.
    """
    value = os.getenv('QUART_DEBUG', None)

    if value is None:
        return 'development' == get_env()

    return value.lower() not in {'0', 'false', 'no'}


def get_env(default: Optional[str]= 'production') -> str:
    """Reads QUART_ENV environment variable to determine in which environment
    the app is running on. Defaults to 'production' when unset.
    """
    return os.getenv('QUART_ENV', default)


async def make_response(*args: Any) -> Response:
    """Create a response, a simple wrapper function.

    This is most useful when you want to alter a Response before
    returning it, for example

    .. code-block:: python

        response = make_response(render_template('index.html'))
        response.headers['X-Header'] = 'Something'

    """
    if not args:
        return current_app.response_class()
    if len(args) == 1:
        args = args[0]

    return await current_app.make_response(args)


async def make_push_promise(path: str) -> None:
    """Create a push promise, a simple wrapper function.

    This takes a path that should be pushed to the client if the
    protocol is HTTP/2.

    """
    return await request.send_push_promise(path)


async def flash(message: str, category: str='message') -> None:
    """Add a message (with optional category) to the session store.

    This is typically used to flash a message to a user that will be
    stored in the session and shown during some other request. For
    example,

    .. code-block:: python

        @app.route('/login', methods=['POST'])
        async def login():
            ...
            await flash('Login successful')
            return redirect(url_for('index'))

    allows the index route to show the flashed messsages, without
    having to accept the message as an argument or otherwise.  See
    :func:`~quart.helpers.get_flashed_messages` for message retrieval.
    """
    flashes = session.get('_flashes', [])
    flashes.append((category, message))
    session['_flashes'] = flashes
    await message_flashed.send(
        current_app._get_current_object(), message=message, category=category,
    )


def get_flashed_messages(
        with_categories: bool=False,
        category_filter: List[str]=[],
) -> Union[List[str], List[Tuple[str, str]]]:
    """Retrieve the flashed messages stored in the session.

    This is mostly useful in templates where it is exposed as a global
    function, for example

    .. code-block:: html+jinja

        <ul>
        {% for message in get_flashed_messages() %}
          <li>{{ message }}</li>
        {% endfor %}
        </ul>

    Note that caution is required for usage of ``category_filter`` as
    all messages will be popped, but only those matching the filter
    returned. See :func:`~quart.helpers.flash` for message creation.
    """
    flashes = session.pop('_flashes') if '_flashes' in session else []
    if category_filter:
        flashes = [flash for flash in flashes if flash[0] in category_filter]
    if not with_categories:
        flashes = [flash[1] for flash in flashes]
    return flashes


def get_template_attribute(template_name: str, attribute: str) -> Any:
    """Load a attribute from a template.

    This is useful in Python code in order to use attributes in
    templates.

    Arguments:
        template_name: To load the attribute from.
        attribute: The attribute name to load
    """
    return getattr(current_app.jinja_env.get_template(template_name).module, attribute)


def url_for(
        endpoint: str,
        *,
        _anchor: Optional[str]=None,
        _external: Optional[bool]=None,
        _method: Optional[str]=None,
        _scheme: Optional[str]=None,
        **values: Any,
) -> str:
    """Return the url for a specific endpoint.

    This is most useful in templates and redirects to create a URL
    that can be used in the browser.

    Arguments:
        endpoint: The endpoint to build a url for, if prefixed with
            ``.`` it targets endpoint's in the current blueprint.
        _anchor: Additional anchor text to append (i.e. #text).
        _external: Return an absolute url for external (to app) usage.
        _method: The method to consider alongside the endpoint.
        _scheme: A specific scheme to use.
        values: The values to build into the URL, as specified in
            the endpoint rule.
    """
    app_context = _app_ctx_stack.top
    request_context = _request_ctx_stack.top

    if request_context is not None:
        url_adapter = request_context.url_adapter
        if endpoint.startswith('.'):
            if request.blueprint is not None:
                endpoint = request.blueprint + endpoint
            else:
                endpoint = endpoint[1:]
        if _external is None:
            _external = False
    elif app_context is not None:
        url_adapter = app_context.url_adapter
        if _external is None:
            _external = True
    else:
        raise RuntimeError('Cannot create a url outside of an application context')

    if url_adapter is None:
        raise RuntimeError(
            'Unable to create a url adapter, try setting the the SERVER_NAME config variable.'
        )
    if _scheme is not None and not _external:
        raise ValueError('External must be True for scheme usage')

    app_context.app.inject_url_defaults(endpoint, values)
    try:
        url = url_adapter.build(
            endpoint, values, method=_method, scheme=_scheme, external=_external,
        )
    except BuildError as error:
        return app_context.app.handle_url_build_error(error, endpoint, values)

    if _anchor is not None:
        quoted_anchor = quote(_anchor)
        url = f"{url}#{quoted_anchor}"
    return url


def stream_with_context(func: Callable) -> Callable:
    """Share the current request context with a generator.

    This allows the request context to be accessed within a streaming
    generator, for example,

    .. code-block:: python

        @app.route('/')
        def index() -> AsyncGenerator[bytes, None]:
            @stream_with_context
            async def generator() -> bytes:
                yield request.method.encode()
                yield b' '
                yield request.path.encode()

            return generator()

    """
    request_context = _request_ctx_stack.top.copy()

    @wraps(func)
    async def generator(*args: Any, **kwargs: Any) -> Any:
        async with request_context:
            async for data in func(*args, **kwargs):
                yield data
    return generator


def _endpoint_from_view_func(view_func: Callable) -> str:
    return view_func.__name__


def find_package(name: str) -> Tuple[Optional[Path], Path]:
    """Finds packages install prefix (or None) and it's containing Folder
    """
    module = name.split(".")[0]
    loader = pkgutil.get_loader(module)
    if name == "__main__" or loader is None:
        package_path = Path.cwd()
    else:
        if hasattr(loader, 'get_filename'):
            filename = loader.get_filename(module)  # type: ignore
        else:
            __import__(name)
            filename = sys.modules[name].__file__
        package_path = Path(filename).resolve().parent
        if hasattr(loader, 'is_package'):
            is_package = loader.is_package(module)  # type: ignore
            if is_package:
                package_path = Path(package_path).resolve().parent
    sys_prefix = Path(sys.prefix).resolve()
    try:
        package_path.relative_to(sys_prefix)
    except ValueError:
        return None, package_path
    else:
        return sys_prefix, package_path
