"""
Application for configuring and building the app environments.
"""

import os, re, sys
import logging
from concurrent.futures import ThreadPoolExecutor
from urllib.parse import urlparse

from traitlets import Unicode, Integer, Bool, Dict, validate, Any, default, observe, List, TraitError, Union, Callable
from traitlets.config import Application, catch_config_error, SingletonConfigurable
from tornado.httpclient import AsyncHTTPClient
from tornado.httpserver import HTTPServer
import tornado.ioloop
import tornado.options
import tornado.log
from sqlalchemy.exc import OperationalError
from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader
from jupyterhub.services.auth import HubOAuthCallbackHandler
from jupyterhub import __version__ as __jh_version__
from jupyterhub import dbutil
from jupyterhub.traitlets import EntryPointType

from .dashboard import DashboardRepr
from .util import url_path_join
from jupyterhub import orm as jhorm
from .builder.builders import BuildersDict, Builder
from ._data import DATA_FILES_PATH
from .pluggymanager import pm
from . import hookimpl


_TEMPLATE_PATH_OPEN = os.path.join(DATA_FILES_PATH, 'templates-open')
_TEMPLATE_PATH_RESTRICTED = os.path.join(DATA_FILES_PATH, 'templates-restricted')
_TEMPLATE_PATH_COMMON = os.path.join(DATA_FILES_PATH, 'templates-common')


CDS_TEMPLATE_PATHS = [_TEMPLATE_PATH_OPEN, _TEMPLATE_PATH_COMMON]
CDS_TEMPLATE_PATHS_RESTRICTED = [_TEMPLATE_PATH_RESTRICTED, _TEMPLATE_PATH_COMMON]

_all_allowed_presentation_types = ['voila', 'streamlit', 'plotlydash', 'bokeh', 'panel', 'rshiny']


class CDSDashboardsConfig(SingletonConfigurable):

    builder_class = EntryPointType(
        default_value='cdsdashboards.builder.builders.Builder',
        klass=Builder,
        entry_point_group="cdsdashboards.builders",
        help="""The class to use for building dashboard servers.

        Should be a subclass of :class:`cdsdashboards.builder.builders.Builder`.

        May be registered via entry points,
            e.g. `c.cdsdashboards.builders = 'localprocess'`
        """,
    ).tag(config=True)

    server_name_template = Unicode(
        'dash-{urlname}',
        help="""
        How to name the final user server that runs the dashboard. Template vars will be expanded:

        {urlname} : dashboard URL-safe name
        {date} : <current date in YYmmdd format>
        {time} : <current date in HHMMSS format>

        """
    ).tag(config=True)

    presentation_types = List(
        trait=Unicode,
        default_value=_all_allowed_presentation_types,
        minlen=1,
        help="""
        Allowed presentation types for Dashboards. A list, allowed strings are: {}.
        There must be at least one valid entry.
        Default value is all the allowed presentation types.
        Add any custom frameworks to the extra_presentation_types config if you want to augment instead of overwrite the default list.
        """.format(_all_allowed_presentation_types)
    ).tag(config=True)

    extra_presentation_types = List(
        trait=Unicode,
        default_value=[],
        minlen=0,
        help="""
        Extra custom presentation types for Dashboards, to be added to the presentation_types list.
        A list.
        Default value is the empty list.
        """
    ).tag(config=True)

    @property
    def merged_presentation_types(self):
        return self.presentation_types + self.extra_presentation_types

    show_source_git = Bool(
        True,
        help="""
        Allow the user to enter a git repo to fetch files for a dashboard.
        """
    ).tag(config=True)

    default_allow_all = Bool(
        True,
        help="""
        If True (default) then newly-created Dashboards will be accessible to all authenticated JupyterHub users.
        If False, only members of the dashboard's own group will be allowed to access it.
        This flag is passed on to the Dashboard object's allow_all field so can be subsequently overridden in the database.
        """
    ).tag(config=True)

    conda_envs = Union(
        [
            List(trait=Unicode, default_value=[], minlen=0),
            Callable()
        ],
        help="""
        A list of Conda env names for the dashboard creator to select.
        A list.
        Default value is the empty list. Also accepts conda_envs to
        be a callable function which is evaluated for each render of
        the dashboard page.
        """
    ).tag(config=True)

    allow_custom_conda_env = Bool(
        False,
        help="""
        If True then dashboard creators can type any value for the Conda env to use, even if none is provided in the conda_envs list of pre-defined env names.
        If False (default), only Conda env names listed in the conda_envs setting can be selected - if any are present.
        """
    ).tag(config=True)

    spawn_allow_group = Unicode(
        '',
        help="""
        Name of a JupyterHub group whose users should be allowed to spawn servers and create dashboards.
        See also spawn_block_group.
        """
    ).tag(config=True)

    spawn_block_group = Unicode(
        '',
        help="""
        Name of a JupyterHub group whose users should be blocked from spawning servers and creating dashboards.
        If blank, spawn_allow_group will determine which users should be able to spawn.
        If both settings are blank, all users will be allowed to spawn.
        If both are non-blank, spawn_block_group will take priority.
        """
    ).tag(config=True)

    include_auth_state = Bool(
        False,
        help="""
        Whether to include `auth_state` as part of the user information returned by
        dashboards-api/hub-info/user endpoint; default False.

        Warning: `auth_state` can contained sensitive information about the user
        (like credentials).
        """
    ).tag(config=True)

    include_servers = Bool(
        False,
        help="""
        Whether to include the user servers as part of the user information returned by
        dashboards-api/hub-info/user endpoint; default False.
        """
    ).tag(config=True)

    include_servers_state = Bool(
        False,
        help="""
        Whether to include the user servers state as part of the user information returned by
        dashboards-api/hub-info/user endpoint; default False.
        """
    ).tag(config=True)

    spawn_default_options = Bool(
        True,
        help="""
        This applies when the spawner may display options to the user before starting a server.
        If True, a Dashboard will always be started with the default spawner options, so no form will be displayed
        and the dashboard will start without waiting.
        If False (and form options are available in the spawner) then the Dashboard will not start until the
        dashboard creator has accessed the dashboard and provided options to the spawner.
        """
    ).tag(config=True)

    spawn_as_viewer = Bool(
        False,
        help="""
        If True, spawns dashboard as a template owned by the viewing user and accessible only by the viewer.
        Runs multiple dashboard servers per dashboard.
        """
    ).tag(config=True)

    jupyter_startpath_regex = Unicode(
        "",
        help="""
        If not blank, used as a regex that the Start Path of a dashboard must match (if using Jupyter Tree rather than Git Repo as a source).
        This is checked only when a dashboard is created or edited (through the form).
        """
    ).tag(config=True)

class CDSConfigStore():

    _instance = None

    @classmethod
    def get_instance(cls, config):
        """
        Supply a config object to get the singleton CDSDashboardsConfig instance - only normally available from web handlers
        """
        if cls._instance:
            return cls._instance

        cls._instance = CDSDashboardsConfig(config=config)

        return cls._instance


class BuildersStore():

    _instance = None

    @classmethod
    def get_instance(cls, config):
        """
        Supply a config object to get the singleton instance - only normally available from web handlers
        """
        if cls._instance:
            return cls._instance

        cdsconfig = CDSConfigStore.get_instance(config)

        builder_class = cdsconfig.builder_class

        def builder_factory(dashboard):
            return builder_class(dashboard=dashboard, cdsconfig=cdsconfig)

        cls._instance = BuildersDict(builder_factory)
        return cls._instance


cds_tornado_settings = {}
