import collections
import importlib
import os
import re
import sys
from fnmatch import fnmatch
from pathlib import Path
from os.path import isfile, join
from urllib.parse import parse_qs

import flask

from . import _validate
from ._utils import AttributeDict
from ._get_paths import get_relative_path
from ._callback_context import context_value
from ._get_app import get_app


CONFIG = AttributeDict()
PAGE_REGISTRY = collections.OrderedDict()


def _infer_image(module):
    """
    Return:
    - A page specific image: `assets/<module>.<extension>` is used, e.g. `assets/weekly_analytics.png`
    - A generic app image at `assets/app.<extension>`
    - A logo at `assets/logo.<extension>`
    """
    assets_folder = CONFIG.assets_folder
    valid_extensions = ["apng", "avif", "gif", "jpeg", "jpg", "png", "svg", "webp"]
    page_id = module.split(".")[-1]
    files_in_assets = []

    if os.path.exists(assets_folder):
        files_in_assets = [
            f for f in os.listdir(assets_folder) if isfile(join(assets_folder, f))
        ]
    app_file = None
    logo_file = None
    for fn in files_in_assets:
        fn_without_extension, _, extension = fn.partition(".")
        if extension.lower() in valid_extensions:
            if (
                fn_without_extension == page_id
                or fn_without_extension == page_id.replace("_", "-")
            ):
                return fn

            if fn_without_extension == "app":
                app_file = fn

            if fn_without_extension == "logo":
                logo_file = fn

    if app_file:
        return app_file

    return logo_file


def _module_name_to_page_name(module_name):
    return module_name.split(".")[-1].replace("_", " ").capitalize()


def _infer_path(module_name, template):
    if template is None:
        if CONFIG.pages_folder:
            pages_module = str(Path(CONFIG.pages_folder).name)
            path = (
                module_name.split(pages_module)[-1]
                .replace("_", "-")
                .replace(".", "/")
                .lower()
            )
        else:
            path = module_name.replace("_", "-").replace(".", "/").lower()
    else:
        # replace the variables in the template with "none" to create a default path if
        # no path is supplied
        path = re.sub("<.*?>", "none", template)
    path = "/" + path if not path.startswith("/") else path
    return path


def _module_name_is_package(module_name):
    return (
        module_name in sys.modules
        and Path(sys.modules[module_name].__file__).name == "__init__.py"
    )


def _path_to_module_name(path):
    return str(path).replace(".py", "").strip(os.sep).replace(os.sep, ".")


def _infer_module_name(page_path):
    relative_path = page_path.split(CONFIG.pages_folder)[-1]
    module = _path_to_module_name(relative_path)
    proj_root = flask.helpers.get_root_path(CONFIG.name)
    if CONFIG.pages_folder.startswith(proj_root):
        parent_path = CONFIG.pages_folder[len(proj_root) :]
    else:
        parent_path = CONFIG.pages_folder
    parent_module = _path_to_module_name(parent_path)

    module_name = f"{parent_module}.{module}"
    if _module_name_is_package(CONFIG.name):
        # Only prefix with CONFIG.name when it's an imported package name
        module_name = f"{CONFIG.name}.{module_name}"
    return module_name


def _parse_query_string(search):
    if search and len(search) > 0 and search[0] == "?":
        search = search[1:]
    else:
        return {}

    parsed_qs = {}
    for (k, v) in parse_qs(search).items():
        v = v[0] if len(v) == 1 else v
        parsed_qs[k] = v
    return parsed_qs


def _parse_path_variables(pathname, path_template):
    """
    creates the dict of path variables passed to the layout
    e.g. path_template= "/asset/<asset_id>"
         if pathname provided by the browser is "/assets/a100"
         returns **{"asset_id": "a100"}
    """

    # parse variable definitions e.g. <var_name> from template
    # and create pattern to match
    wildcard_pattern = re.sub("<.*?>", "*", path_template)
    var_pattern = re.sub("<.*?>", "(.*)", path_template)

    # check that static sections of the pathname match the template
    if not fnmatch(pathname, wildcard_pattern):
        return None

    # parse variable names e.g. var_name from template
    var_names = re.findall("<(.*?)>", path_template)

    # parse variables from path
    variables = re.findall(var_pattern, pathname)
    variables = variables[0] if isinstance(variables[0], tuple) else variables

    return dict(zip(var_names, variables))


def _create_redirect_function(redirect_to):
    def redirect():
        return flask.redirect(redirect_to, code=301)

    return redirect


def _set_redirect(redirect_from, path):
    app = get_app()
    if redirect_from and len(redirect_from):
        for redirect in redirect_from:
            fullname = app.get_relative_path(redirect)
            app.server.add_url_rule(
                fullname,
                fullname,
                _create_redirect_function(app.get_relative_path(path)),
            )


def register_page(
    module,
    path=None,
    path_template=None,
    name=None,
    order=None,
    title=None,
    description=None,
    image=None,
    image_url=None,
    redirect_from=None,
    layout=None,
    **kwargs,
):
    """
    Assigns the variables to `dash.page_registry` as an `OrderedDict`
    (ordered by `order`).

    `dash.page_registry` is used by `pages_plugin` to set up the layouts as
    a multi-page Dash app. This includes the URL routing callbacks
    (using `dcc.Location`) and the HTML templates to include title,
    meta description, and the meta description image.

    `dash.page_registry` can also be used by Dash developers to create the
    page navigation links or by template authors.

    - `module`:
       The module path where this page's `layout` is defined. Often `__name__`.

    - `path`:
       URL Path, e.g. `/` or `/home-page`.
       If not supplied, will be inferred from the `path_template` or `module`,
       e.g. based on path_template: `/asset/<asset_id` to `/asset/none`
       e.g. based on module: `pages.weekly_analytics` to `/weekly-analytics`

    - `relative_path`:
        The path with `requests_pathname_prefix` prefixed before it.
        Use this path when specifying local URL paths that will work
        in environments regardless of what `requests_pathname_prefix` is.
        In some deployment environments, like Dash Enterprise,
        `requests_pathname_prefix` is set to the application name,
        e.g. `my-dash-app`.
        When working locally, `requests_pathname_prefix` might be unset and
        so a relative URL like `/page-2` can just be `/page-2`.
        However, when the app is deployed to a URL like `/my-dash-app`, then
        `relative_path` will be `/my-dash-app/page-2`.

    - `path_template`:
       Add variables to a URL by marking sections with <variable_name>. The layout function
       then receives the <variable_name> as a keyword argument.
       e.g. path_template= "/asset/<asset_id>"
            then if pathname in browser is "/assets/a100" then layout will receive **{"asset_id":"a100"}

    - `name`:
       The name of the link.
       If not supplied, will be inferred from `module`,
       e.g. `pages.weekly_analytics` to `Weekly analytics`

    - `order`:
       The order of the pages in `page_registry`.
       If not supplied, then the filename is used and the page with path `/` has
       order `0`

    - `title`:
       (string or function) The name of the page <title>. That is, what appears in the browser title.
       If not supplied, will use the supplied `name` or will be inferred by module,
       e.g. `pages.weekly_analytics` to `Weekly analytics`

    - `description`:
       (string or function) The <meta type="description"></meta>.
       If not supplied, then nothing is supplied.

    - `image`:
       The meta description image used by social media platforms.
       If not supplied, then it looks for the following images in `assets/`:
        - A page specific image: `assets/<module>.<extension>` is used, e.g. `assets/weekly_analytics.png`
        - A generic app image at `assets/app.<extension>`
        - A logo at `assets/logo.<extension>`
        When inferring the image file, it will look for the following extensions:
        APNG, AVIF, GIF, JPEG, JPG, PNG, SVG, WebP.

    -  `image_url`:
       Overrides the image property and sets the `<image>` meta tag to the provided image URL.

    - `redirect_from`:
       A list of paths that should redirect to this page.
       For example: `redirect_from=['/v2', '/v3']`

    - `layout`:
       The layout function or component for this page.
       If not supplied, then looks for `layout` from within the supplied `module`.

    - `**kwargs`:
       Arbitrary keyword arguments that can be stored

    ***

    `page_registry` stores the original property that was passed in under
    `supplied_<property>` and the coerced property under `<property>`.
    For example, if this was called:
    ```
    register_page(
        'pages.historical_outlook',
        name='Our historical view',
        custom_key='custom value'
    )
    ```
    Then this will appear in `page_registry`:
    ```
    OrderedDict([
        (
            'pages.historical_outlook',
            dict(
                module='pages.historical_outlook',

                supplied_path=None,
                path='/historical-outlook',

                supplied_name='Our historical view',
                name='Our historical view',

                supplied_title=None,
                title='Our historical view'

                supplied_layout=None,
                layout=<function pages.historical_outlook.layout>,

                custom_key='custom value'
            )
        ),
    ])
    ```
    """
    if context_value.get().get("ignore_register_page"):
        return

    _validate.validate_use_pages(CONFIG)

    page = dict(
        module=_validate.validate_module_name(module),
        supplied_path=path,
        path_template=path_template,
        path=path if path is not None else _infer_path(module, path_template),
        supplied_name=name,
        name=name if name is not None else _module_name_to_page_name(module),
    )
    page.update(
        supplied_title=title,
        title=(title if title is not None else page["name"]),
    )
    page.update(
        description=description if description else "",
        order=order,
        supplied_order=order,
        supplied_layout=layout,
        **kwargs,
    )
    page.update(
        supplied_image=image,
        image=(image if image is not None else _infer_image(module)),
        image_url=image_url,
    )
    page.update(redirect_from=_set_redirect(redirect_from, page["path"]))

    PAGE_REGISTRY[module] = page

    if page["path_template"]:
        _validate.validate_template(page["path_template"])

    if layout is not None:
        # Override the layout found in the file set during `plug`
        PAGE_REGISTRY[module]["layout"] = layout

    # set home page order
    order_supplied = any(
        p["supplied_order"] is not None for p in PAGE_REGISTRY.values()
    )

    for p in PAGE_REGISTRY.values():
        p["order"] = (
            0 if p["path"] == "/" and not order_supplied else p["supplied_order"]
        )
        p["relative_path"] = get_relative_path(p["path"])

    # Sort numeric orders first, then string orders, then no order,
    # finally by module name for matching orders
    for page in sorted(
        PAGE_REGISTRY.values(),
        key=lambda i: (
            i["order"] is None,  # False (order given) sorts before True
            i["order"] if isinstance(i["order"], (int, float)) else float("inf"),
            str(i["order"]),
            i["module"],
        ),
    ):
        PAGE_REGISTRY.move_to_end(page["module"])


def _path_to_page(path_id):
    path_variables = None
    for page in PAGE_REGISTRY.values():
        if page["path_template"]:
            template_id = page["path_template"].strip("/")
            path_variables = _parse_path_variables(path_id, template_id)
            if path_variables:
                return page, path_variables
        if path_id == page["path"].strip("/"):
            return page, path_variables
    return {}, None


def _page_meta_tags(app):
    start_page, path_variables = _path_to_page(flask.request.path.strip("/"))

    # use the supplied image_url or create url based on image in the assets folder
    image = start_page.get("image", "")
    if image:
        image = app.get_asset_url(image)
    assets_image_url = (
        "".join([flask.request.url_root, image.lstrip("/")]) if image else None
    )
    supplied_image_url = start_page.get("image_url")
    image_url = supplied_image_url if supplied_image_url else assets_image_url

    title = start_page.get("title", app.title)
    if callable(title):
        title = title(**path_variables) if path_variables else title()

    description = start_page.get("description", "")
    if callable(description):
        description = description(**path_variables) if path_variables else description()

    return [
        {"name": "description", "content": description},
        {"property": "twitter:card", "content": "summary_large_image"},
        {"property": "twitter:url", "content": flask.request.url},
        {"property": "twitter:title", "content": title},
        {"property": "twitter:description", "content": description},
        {"property": "twitter:image", "content": image_url or ""},
        {"property": "og:title", "content": title},
        {"property": "og:type", "content": "website"},
        {"property": "og:description", "content": description},
        {"property": "og:image", "content": image_url or ""},
    ]


def _import_layouts_from_pages(pages_folder):
    for root, dirs, files in os.walk(pages_folder):
        dirs[:] = [d for d in dirs if not d.startswith(".") and not d.startswith("_")]
        for file in files:
            if file.startswith("_") or file.startswith(".") or not file.endswith(".py"):
                continue
            page_path = os.path.join(root, file)
            with open(page_path, encoding="utf-8") as f:
                content = f.read()
                if "register_page" not in content:
                    continue

            module_name = _infer_module_name(page_path)
            spec = importlib.util.spec_from_file_location(module_name, page_path)
            page_module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(page_module)
            sys.modules[module_name] = page_module

            if (
                module_name in PAGE_REGISTRY
                and not PAGE_REGISTRY[module_name]["supplied_layout"]
            ):
                _validate.validate_pages_layout(module_name, page_module)
                PAGE_REGISTRY[module_name]["layout"] = getattr(page_module, "layout")
