"""
Facilities to interface with the Heliophysics Events Knowledgebase.
"""
import json
import codecs
import urllib
import inspect
from itertools import chain

import astropy.table
from astropy.table import Row

import sunpy.net._attrs as core_attrs
from sunpy import log
from sunpy.net import attr
from sunpy.net.base_client import BaseClient, QueryResponseTable
from sunpy.net.hek import attrs
from sunpy.net.hek.utils import (
    _freeze,
    _map_chain_code_columns_to_coordinates,
    _map_columns_to_quantities,
    _map_columns_to_times,
    _map_event_coord_columns_to_coordinates,
)
from sunpy.util import dict_keys_same, unique
from sunpy.util.xml import xml_to_dict

__all__ = ['HEKClient', 'HEKTable', 'HEKRow']

DEFAULT_URL = 'https://www.lmsal.com/hek/her?'

class HEKClient(BaseClient):
    """
    Provides access to the Heliophysics Event Knowledgebase (HEK).

    The HEK stores solar feature and event data generated by algorithms and
    human observers.

    .. note::

        sunpy parses the raw outputs from the HEK by add units to some columns,
        returning `astropy.time.Time` objects instead of strings, and converting coordinate
        information to `~astropy.coordinates.SkyCoord` objects wherever possible.

    .. note::

        As part of the aforementioned parsing, some columns are dropped from the returned
        table because they are now redundant. For example, columns ``event_coord1``,
        ``event_coord2`` and ``event_coord3`` are merged into one new column called ``event_coord``
        which is a `~astropy.coordinates.SkyCoord`. Additionally, columns representing units are
        dropped because the unit is now attached to the column representing the actual value.

    .. note::

        If you want the raw HEK output, you can access it via the ``raw`` attribute of the
        `~sunpy.net.hek.HEKTable` object returned by the search method.

    References
    ----------
    * `Heliophysics Knowledge Base Feature/Event Types definitions <https://www.lmsal.com/hek/VOEvent_Spec.html>`__
    """
    # FIXME: Expose fields in .attrs with the right types
    # that is, not all StringParamWrapper!

    default = {
        'cosec': '2',  # Return .json
        'cmd': 'search',
        'type': 'column',
        'event_type': '**',
    }
    # Default to full disk.
    attrs.walker.apply(attrs.SpatialRegion(), {}, default)

    def __init__(self, url=DEFAULT_URL):
        self.url = url

    def _download(self, data):
        """ Download all data, even if paginated. """
        page = 1
        results = []
        new_data = data.copy()
        # Override the default name of the operatorX, where X is a number.
        for key in data.keys():
            if "operator" in key:
                new_data[f"op{key.split('operator')[-1]}"] = new_data.pop(key)
        while True:
            new_data['page'] = page
            url = self.url + urllib.parse.urlencode(new_data)
            log.debug(f'Opening {url}')
            fd = urllib.request.urlopen(url)
            try:
                result = codecs.decode(fd.read(), encoding='utf-8', errors='replace')
                result = json.loads(result)
            except Exception as e:
                raise OSError("Failed to load return from the HEKClient.") from e
            finally:
                fd.close()
            results.extend(result['result'])
            if not result['overmax']:
                if len(results) > 0:
                    return astropy.table.Table(dict_keys_same(results))
                else:
                    return astropy.table.Table()
            page += 1


    def search(self, *args, **kwargs):
        """
        Retrieves information about HEK records matching the criteria
        given in the query expression. If multiple arguments are passed,
        they are connected with AND. The result of a query is a list of
        unique HEK Response objects that fulfill the criteria.

        Examples
        -------
        >>> from sunpy.net import attrs as a, Fido
        >>> timerange = a.Time('2011/08/09 07:23:56', '2011/08/09 12:40:29')
        >>> res = Fido.search(timerange, a.hek.FL, a.hek.FRM.Name == "SWPC")  # doctest: +REMOTE_DATA
        >>> res  # doctest: +SKIP
        <sunpy.net.fido_factory.UnifiedResponse object at ...>
        Results from 1 Provider:
        <BLANKLINE>
        2 Results from the HEKClient:
                 SOL_standard          active ... skel_startc2 sum_overlap_scores
        ------------------------------ ------ ... ------------ ------------------
        SOL2011-08-09T07:19:00L227C090   true ...         None                  0
        SOL2011-08-09T07:48:00L296C073   true ...         None                  0
        <BLANKLINE>
        <BLANKLINE>
        """
        query = attr.and_(*args)
        data = attrs.walker.create(query, {})
        ndata = []
        for elem in data:
            new = self.default.copy()
            new.update(elem)
            ndata.append(new)
        if len(ndata) == 1:
            return HEKTable._from_search(self._download(ndata[0]), client=self)
        else:
            return HEKTable._from_search(self._merge(self._download(data) for data in ndata), client=self)

    def _merge(self, responses):
        """ Merge responses, removing duplicates. """
        return astropy.table.vstack(list(unique(chain.from_iterable(responses), _freeze)))

    def fetch(self, *args, **kwargs):
        """
        This is a no operation function as this client does not download data.
        """
        return NotImplemented

    @classmethod
    def _attrs_module(cls):
        return 'hek', 'sunpy.net.hek.attrs'

    @classmethod
    def _can_handle_query(cls, *query):
        required = {core_attrs.Time}
        optional = {i[1] for i in inspect.getmembers(attrs, inspect.isclass)} - required
        qr = tuple(x for x in query if not isinstance(x, attrs.EventType))
        return cls.check_attr_types_in_query(qr, required, optional)


class HEKRow(Row):
    """
    Handles the response from the HEK. Each HEKRow object is a subclass
    of `~astropy.table.Row`. The column-row key-value pairs correspond to the
    HEK feature/event properties and their values, for that record from the
    HEK.  Each HEKRow object also has extra properties that relate HEK
    concepts to VSO concepts.
    """
    @property
    def vso_time(self):
        return core_attrs.Time(
            self['event_starttime'],
            self['event_endtime']
        )

    @property
    def vso_instrument(self):
        if self['obs_instrument'] == 'HEK':
            raise ValueError("No instrument contained.")
        return core_attrs.Instrument(self['obs_instrument'])

    @property
    def vso_all(self):
        return attr.and_(self.vso_time, self.vso_instrument)

    def get_voevent(self, as_dict=True,
                    base_url="https://www.lmsal.com/hek/her?"):
        """Retrieves the VOEvent object associated with a given event and
        returns it as either a Python dictionary or an XML string."""

        # Build URL
        params = {
            "cmd": "export-voevent",
            "cosec": 1,
            "ivorn": self['kb_archivid']
        }
        url = base_url + urllib.parse.urlencode(params)

        # Query and read response
        response = urllib.request.urlopen(url).read()

        # Return a string or dict
        if as_dict:
            return xml_to_dict(response)
        else:
            return response

    def get(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default


class HEKTable(QueryResponseTable):
    """
    A container for data returned from HEK searches.
    """
    Row = HEKRow

    @classmethod
    def _from_search(cls, data, **kwargs):
        """
        Create table from HEK search results. This preserves the original HEK response
        as well as parsing the various coordinate, time, and quantity columns.
        """
        # Preserve unparsed HEK output
        raw_data = data.copy()
        if len(data) > 0:
            # NOTE: The order of operations here is important.
            _map_event_coord_columns_to_coordinates(data)
            _map_chain_code_columns_to_coordinates(data)
            _map_columns_to_quantities(data)
            _map_columns_to_times(data)
        instance = cls(data=data, **kwargs)
        instance.raw = raw_data
        return instance
