import re
from collections import namedtuple

from . import _client, _definitions
from .facade import ReturnMapping, Type, TypeEncoder

__all__ = [
    'Delta',
    'Number',
    'Binary',
    'ConfigValue',
    'Resource',
]

__patches__ = [
    'ResourcesFacade',
    'AllWatcherFacade',
    'ActionFacade',
]


class Delta(Type):
    """A single websocket delta.

    :ivar entity: The entity name, e.g. 'unit', 'application'
    :vartype entity: str

    :ivar type: The delta type, e.g. 'add', 'change', 'remove'
    :vartype type: str

    :ivar data: The raw delta data
    :vartype data: dict

    NOTE: The 'data' variable above is being incorrectly cross-linked by a
    Sphinx bug: https://github.com/sphinx-doc/sphinx/issues/2549

    """
    _toSchema = {'deltas': 'deltas'}
    _toPy = {'deltas': 'deltas'}

    def __init__(self, deltas=None):
        """
        :param deltas: [str, str, object]

        """
        self.deltas = deltas

        Change = namedtuple('Change', 'entity type data')
        change = Change(*self.deltas)

        self.entity = change.entity
        self.type = change.type
        self.data = change.data

    @classmethod
    def from_json(cls, data):
        return cls(deltas=data)


class ResourcesFacade(Type):
    """Patch parts of ResourcesFacade to make it work.
    """

    @ReturnMapping(_client.AddPendingResourcesResult)
    async def AddPendingResources(self, application_tag, charm_url, resources):
        """Fix the calling signature of AddPendingResources.

        The ResourcesFacade doesn't conform to the standard facade pattern in
        the Juju source, which leads to the schemagened code not matching up
        properly with the actual calling convention in the API.  There is work
        planned to fix this in Juju, but we have to work around it for now.

        application_tag : str
        charm_url : str
        resources : typing.Sequence<+T_co>[~CharmResource]<~CharmResource>
        Returns -> typing.Union[_ForwardRef('ErrorResult'),
                                typing.Sequence<+T_co>[str]]
        """
        # map input types to rpc msg
        _params = dict()
        msg = dict(type='Resources',
                   request='AddPendingResources',
                   version=1,
                   params=_params)
        _params['tag'] = application_tag
        _params['url'] = charm_url
        _params['resources'] = resources
        reply = await self.rpc(msg)
        return reply


class AllWatcherFacade(Type):
    """
    Patch rpc method of allwatcher to add in 'id' stuff.

    """
    async def rpc(self, msg):
        if not hasattr(self, 'Id'):
            client = _client.ClientFacade.from_connection(self.connection)

            result = await client.WatchAll()
            self.Id = result.watcher_id

        msg['Id'] = self.Id
        result = await self.connection.rpc(msg, encoder=TypeEncoder)
        return result


class ActionFacade(Type):

    class _FindTagsResults(Type):
        _toSchema = {'matches': 'matches'}
        _toPy = {'matches': 'matches'}

        def __init__(self, matches=None, **unknown_fields):
            '''
            FindTagsResults wraps the mapping between the requested prefix and the
            matching tags for each requested prefix.

            Matches map[string][]Entity `json:"matches"`
            '''
            self.matches = {}
            matches = matches or {}
            for prefix, tags in matches.items():
                self.matches[prefix] = [_definitions.Entity.from_json(r)
                                        for r in tags]

    @ReturnMapping(_FindTagsResults)
    async def FindActionTagsByPrefix(self, prefixes):
        '''
        prefixes : typing.Sequence[str]
        Returns -> typing.Sequence[~Entity]
        '''
        # map input types to rpc msg
        _params = dict()
        msg = dict(type='Action',
                   request='FindActionTagsByPrefix',
                   version=2,
                   params=_params)
        _params['prefixes'] = prefixes
        reply = await self.rpc(msg)
        return reply


class Number(_definitions.Number):
    """
    This type represents a semver string.

    Because it is not standard JSON, the typical from_json parsing fails and
    the parsing must be handled specially.

    See https://github.com/juju/version for more info.
    """
    numberPat = re.compile(r'^(\d{1,9})\.(\d{1,9})(?:\.|-([a-z]+))(\d{1,9})(\.\d{1,9})?$')  # noqa

    def __init__(self, major=None, minor=None, patch=None, tag=None,
                 build=None, **unknown_fields):
        '''
        major : int
        minor : int
        patch : int
        tag : str
        build : int
        '''
        self.major = int(major or '0')
        self.minor = int(minor or '0')
        self.patch = int(patch or '0')
        self.tag = tag or ''
        self.build = int(build or '0')

    def __repr__(self):
        return '<Number major={} minor={} patch={} tag={} build={}>'.format(
            self.major, self.minor, self.patch, self.tag, self.build)

    def __str__(self):
        return self.serialize()

    @property
    def _cmp(self):
        return (self.major, self.minor, self.tag, self.patch, self.build)

    def __eq__(self, other):
        return isinstance(other, type(self)) and self._cmp == other._cmp

    def __lt__(self, other):
        return self._cmp < other._cmp

    def __le__(self, other):
        return self._cmp <= other._cmp

    def __gt__(self, other):
        return self._cmp > other._cmp

    def __ge__(self, other):
        return self._cmp >= other._cmp

    @classmethod
    def from_json(cls, data):
        parsed = None
        if isinstance(data, cls):
            return data
        elif data is None:
            return cls()
        elif isinstance(data, dict):
            parsed = data
        elif isinstance(data, str):
            match = cls.numberPat.match(data)
            if match:
                parsed = {
                    'major': match.group(1),
                    'minor': match.group(2),
                    'tag': match.group(3),
                    'patch': match.group(4),
                    'build': (match.group(5)[1:] if match.group(5)
                              else 0),
                }
        if not parsed:
            raise TypeError('Unable to parse Number version string: '
                            '{}'.format(data))
        d = {}
        for k, v in parsed.items():
            d[cls._toPy.get(k, k)] = v

        return cls(**d)

    def serialize(self):
        s = ""
        if not self.tag:
            s = "{}.{}.{}".format(self.major, self.minor, self.patch)
        else:
            s = "{}.{}-{}{}".format(self.major, self.minor, self.tag,
                                    self.patch)
        if self.build:
            s = "{}.{}".format(s, self.build)
        return s

    def to_json(self):
        return self.serialize()


class Binary(_definitions.Binary):
    """
    This type represents a semver string with additional series and arch info.

    Because it is not standard JSON, the typical from_json parsing fails and
    the parsing must be handled specially.

    See https://github.com/juju/version for more info.
    """
    binaryPat = re.compile(r'^(\d{1,9})\.(\d{1,9})(?:\.|-([a-z]+))(\d{1,9})(\.\d{1,9})?-([^-]+)-([^-]+)$')  # noqa

    def __init__(self, number=None, series=None, arch=None, **unknown_fields):
        '''
        number : Number
        series : str
        arch : str
        '''
        self.number = Number.from_json(number)
        self.series = series
        self.arch = arch

    def __repr__(self):
        return '<Binary number={} series={} arch={}>'.format(
            self.number, self.series, self.arch)

    def __str__(self):
        return self.serialize()

    def __eq__(self, other):
        return (
            isinstance(other, type(self)) and
            other.number == self.number and
            other.series == self.series and
            other.arch == self.arch)

    @classmethod
    def from_json(cls, data):
        parsed = None
        if isinstance(data, cls):
            return data
        elif data is None:
            return cls()
        elif isinstance(data, dict):
            parsed = data
        elif isinstance(data, str):
            match = cls.binaryPat.match(data)
            if match:
                parsed = {
                    'number': {
                        'major': match.group(1),
                        'minor': match.group(2),
                        'tag': match.group(3),
                        'patch': match.group(4),
                        'build': (match.group(5)[1:] if match.group(5)
                                  else 0),
                    },
                    'series': match.group(6),
                    'arch': match.group(7),
                }
        if parsed is None:
            raise TypeError('Unable to parse Binary version string: '
                            '{}'.format(data))
        d = {}
        for k, v in parsed.items():
            d[cls._toPy.get(k, k)] = v

        return cls(**d)

    def serialize(self):
        return "{}-{}-{}".format(self.number.serialize(),
                                 self.series, self.arch)

    def to_json(self):
        return self.serialize()


class ConfigValue(_definitions.ConfigValue):
    def __repr__(self):
        return '<{} source={} value={}>'.format(type(self).__name__,
                                                repr(self.source),
                                                repr(self.value))


class Resource(Type):
    _toSchema = {'application': 'application',
                 'charmresource': 'CharmResource',
                 'id_': 'id',
                 'pending_id': 'pending-id',
                 'timestamp': 'timestamp',
                 'username': 'username',
                 'name': 'name',
                 'origin': 'origin'}
    _toPy = {'CharmResource': 'charmresource',
             'application': 'application',
             'id': 'id_',
             'pending-id': 'pending_id',
             'timestamp': 'timestamp',
             'username': 'username',
             'name': 'name',
             'origin': 'origin'}

    def __init__(self, charmresource=None, application=None, id_=None,
                 pending_id=None, timestamp=None, username=None, name=None,
                 origin=None, **unknown_fields):
        '''
        charmresource : CharmResource
        application : str
        id_ : str
        pending_id : str
        timestamp : str
        username : str
        name: str
        origin : str
        '''
        if charmresource:
            self.charmresource = _client.CharmResource.from_json(charmresource)
        else:
            self.charmresource = None
        self.application = application
        self.id_ = id_
        self.pending_id = pending_id
        self.timestamp = timestamp
        self.username = username
        self.name = name
        self.origin = origin
