# -*- coding: utf-8 -*-

"""Storage for detected and fetched widgets."""

from __future__ import annotations

__all__ = ['PartnerPool', 'POOL', 'UPDATER']

import json
import os
import typing
from urllib import parse
import yaml
from qtpy import QtCore
from anaconda_navigator import config
from anaconda_navigator.utils import extra_collections
from anaconda_navigator.utils.logs.loggers import logger
from anaconda_navigator.utils import workers
from . import core
from . import resources
from . import simple_advertisement

if typing.TYPE_CHECKING:
    import typing_extensions
    from qtpy import QtWidgets

    class PartnerConfiguration(typing_extensions.TypedDict, total=False):
        """Dictionary of an additional configuration, that might be used by advertisements and the application."""

        utm_campaign: str


def __init_configuration_paths() -> typing.MutableSequence[str]:
    """Initialize sequence of paths to search configurations in."""
    result: typing.List[str] = []

    if config.WIN:
        # pylint: disable=import-outside-toplevel
        from anaconda_navigator.external.knownfolders import get_folder_path, FOLDERID  # type: ignore
        result.extend((
            os.path.join(get_folder_path(FOLDERID.ProgramData)[0], 'Anaconda3', 'etc', 'partner.yml'),
            os.path.join(get_folder_path(FOLDERID.ProgramData)[0], 'Miniconda3', 'etc', 'partner.yml'),
            os.path.join(os.path.expanduser('~'), 'Anaconda3', 'etc', 'partner.yml'),
            os.path.join(os.path.expanduser('~'), 'Miniconda3', 'etc', 'partner.yml'),
        ))

    else:
        result.extend((
            os.path.join('/', 'etc', 'anaconda', 'partner.yml'),
            os.path.join('opt', 'anaconda3', 'etc', 'partner.yml'),
            os.path.join('opt', 'miniconda3', 'etc', 'partner.yml'),
            os.path.join(os.path.expanduser('~'), 'anaconda3', 'etc', 'partner.yml'),
            os.path.join(os.path.expanduser('~'), 'miniconda3', 'etc', 'partner.yml'),
        ))

    result.extend((
        os.path.join(config.CONF_PATH, 'partner.yml'),
    ))

    return result


CONFIGURATION_PATHS: 'typing_extensions.Final[typing.Sequence[str]]' = __init_configuration_paths()


class PartnerPool(typing.Mapping[core.PartnerWidgetPlacement, 'QtWidgets.QWidget']):
    """Collection of widgets for partner attribution."""

    __slots__ = ('__content', '__settings')

    def __init__(self) -> None:
        """Initialize new :class:`~PartnerPool` instance."""
        self.__settings: core.PartnerSettings = core.PartnerSettings()
        self.__content: 'typing_extensions.Final[typing.Dict[core.PartnerWidgetPlacement, core.PartnerWidget]]' = {}

    @property
    def settings(self) -> core.PartnerSettings:  # noqa: D401
        """Common configuration that might be used by widgets and different application components."""
        return self.__settings

    def _set_settings(self, value: typing.Union[core.PartnerSettings, 'core.SettingsDict']) -> None:
        """Update value of the `configuration`."""
        if not isinstance(value, core.PartnerSettings):
            value = core.PartnerSettings(value)
        self.__settings = value

    def register(self, widget: core.PartnerWidget) -> None:
        """Register new widget in the pool."""
        self.__content[widget.placement] = widget

    def __getitem__(self, key: core.PartnerWidgetPlacement) -> typing.Optional[QtWidgets.QWidget]:
        """Retrieve widget that should be placed in `key` location."""
        value: typing.Optional[core.PartnerWidget] = self.__content.get(key, None)
        if value is None:
            return None
        return value.widget()

    def __iter__(self) -> typing.Iterator[core.PartnerWidgetPlacement]:
        """Iterate through available location options."""
        return iter(core.PartnerWidgetPlacement)

    def __len__(self) -> int:
        """Total number of """
        return len(core.PartnerWidgetPlacement)


POOL: 'typing_extensions.Final[PartnerPool]' = PartnerPool()


class Updater(QtCore.QObject):
    """Contain update utm parameters functional"""

    sig_updated = QtCore.Signal()

    @staticmethod
    def __read_partner_configuration(paths: typing.Optional[typing.Sequence[str]] = None) -> 'PartnerConfiguration':
        """Check exists file of partner config or return empty default"""
        if paths is None:
            paths = CONFIGURATION_PATHS

        path: str
        for path in paths:
            try:
                stream: typing.TextIO
                with open(path, 'rt', encoding='utf8') as stream:
                    result: 'PartnerConfiguration' = yaml.safe_load(stream)

                    if not isinstance(result, typing.Mapping):
                        logger.exception('broken partner configuration file at %s', path)  # type: ignore
                        continue

                    return result

            except FileNotFoundError:
                continue

            except (OSError, yaml.YAMLError):
                logger.exception('broken partner configuration file at %s', path)
                continue

        return {}

    @workers.Task
    def update(self) -> None:
        """Fetch and apply advertisement configuration to the :data:`~POOL`."""
        configuration: core.ConfigurationDict = {
            'widgets': [],
            'settings': {
                'url_parameters': {},
            },
        }

        # prepare a list of URLs to fetch advertisement information from
        advertisement_urls: extra_collections.OrderedSet[str] = extra_collections.OrderedSet()

        partner_configuration: 'PartnerConfiguration' = self.__read_partner_configuration()
        try:
            advertisement_urls.add(
                f'https://www.anaconda.com/api/navigator/partner/{parse.quote(partner_configuration["utm_campaign"])}',
            )
        except KeyError:
            pass

        advertisement_urls.add(config.CONF.get('main', 'advertisement_url'))
        advertisement_urls.add('https://www.anaconda.com/api/navigator')

        # process advertisement URLs
        advertisement_url: str
        for advertisement_url in advertisement_urls:
            try:
                advertisement_content: typing.Optional[bytes] = resources.load_resource(advertisement_url)
                if not advertisement_content:
                    continue

                # Fetch configuration
                definition: 'simple_advertisement.CompositeAdvertisementDict' = {
                    'placement': core.PartnerWidgetPlacement.BOTTOM_LEFT_CORNER,
                    'advertisements': json.loads(advertisement_content),
                }
                configuration['widgets'].append(definition)

                # Extract utm parameters from the redirect_url
                item: simple_advertisement.SimpleAdvertisementDict
                for item in definition['advertisements']:
                    if 'redirect_url' not in item:
                        continue

                    configuration['settings']['url_parameters'].update(
                        (key, value)
                        for key, value in parse.parse_qsl(parse.urlparse(item['redirect_url']).query)
                        if key.startswith('utm_')
                    )
                    break  # NOTE: remove if we need to combine data from all URLs and not just the first valid one

                # Remember the working API url
                config.CONF.set('main', 'advertisement_url', advertisement_url)
                break

            except BaseException:  # pylint: disable=broad-except
                logger.exception('failed to initialize advertisement')

        # apply `utm_campaign` from `partner_configuration`
        try:
            configuration['settings']['url_parameters']['utm_campaign'] = partner_configuration['utm_campaign']
        except KeyError:
            pass

        # apply default values for required parameters
        configuration['settings']['url_parameters'].setdefault('utm_source', 'anaconda_navigator')

        # make configuration available to everything
        POOL._set_settings(configuration['settings'])  # pylint: disable=protected-access

        # create widgets according to the configuration
        for widget in configuration['widgets']:
            instance: core.PartnerWidget

            # NOTE: to support additional built-in advertisement types:
            #
            #       if widget.get('type', 'simple') == 'simple':
            instance = simple_advertisement.CompositeAdvertisementWidget(
                definition=typing.cast('simple_advertisement.CompositeAdvertisementDict', widget),
                settings=POOL.settings,
            )

            POOL.register(instance)

        # NOTE: NAV-753 should be here
        #
        #       Parse other locations for widgets, initialize them with `POOL.settings` and then `POOL.register` them.

        self.sig_updated.emit()


UPDATER: 'typing_extensions.Final[Updater]' = Updater()
