import copy
import datetime
from abc import ABC, abstractmethod
from collections import defaultdict

import pytz

from fhirpy.base.resource import BaseResource, BaseReference
from fhirpy.base.utils import chunks, encode_params
from fhirpy.base.exceptions import InvalidResponse

FHIR_DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
FHIR_DATE_FORMAT = "%Y-%m-%d"


def format_date_time(date: datetime.datetime):
    return pytz.utc.normalize(date).strftime(FHIR_DATE_TIME_FORMAT)


def format_date(date: datetime.date):
    return date.strftime(FHIR_DATE_FORMAT)


def transform_param(param: str):
    """
    >>> transform_param('general_practitioner')
    'general-practitioner'
    """
    if param[0] == "_" or param[0] == ".":
        # Don't correct _id, _has, _include, .effectiveDate and etc.
        return param

    return param.replace("_", "-")


def transform_value(value):
    """
    >>> transform_value(datetime.datetime(2019, 1, 1, tzinfo=pytz.utc))
    '2019-01-01T00:00:00Z'

    >>> transform_value(datetime.date(2019, 1, 1))
    '2019-01-01'

    >>> transform_value(True)
    'true'
    """
    if isinstance(value, datetime.datetime):
        return format_date_time(value)
    if isinstance(value, datetime.date):
        return format_date(value)
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, (BaseReference, BaseResource)):
        return value.reference
    return value


class Raw:
    kwargs = {}

    def __init__(self, **kwargs):
        self.kwargs = kwargs


def SQ(*args, **kwargs):
    """
    Builds search query

    >>> dict(SQ(general_practitioner='prid'))
    {'general-practitioner': ['prid']}

    >>> dict(SQ(patient__Patient__name='John'))
    {'patient:Patient.name': ['John']}

    >>> dict(SQ(patient__Patient__birth_date__ge='2000'))
    {'patient:Patient.birth-date': ['ge2000']}

    >>> dict(SQ(patient__Patient__general_practitioner__Organization__name='Name'))
    {'patient:Patient.general-practitioner:Organization.name': ['Name']}

    >>> dict(SQ(patient__Patient__general_practitioner__name='Name'))
    {'patient:Patient.general-practitioner.name': ['Name']}

    >>> dict(SQ(based_on__instantiates_canonical='PlanDefinition/id'))
    {'based-on.instantiates-canonical': ['PlanDefinition/id']}

    >>> dict(SQ(period__ge='2018', period__lt='2019'))
    {'period': ['ge2018', 'lt2019']}

    >>> dict(SQ(text__contains='test'))
    {'text:contains': ['test']}

    >>> dict(SQ(url__not_in='http://loinc.org'))
    {'url:not-in': ['http://loinc.org']}

    >>> dict(SQ(name='family1,family2'))
    {'name': ['family1,family2']}

    >>> dict(SQ(status__not=['failed', 'completed']))
    {'status:not': ['failed', 'completed']}

    >>> dict(SQ(active=True))
    {'active': ['true']}

    >>> dict(SQ(**{'_has:Person:link:id': 'id'}))
    {'_has:Person:link:id': ['id']}

    >>> dict(SQ(**{'.effectiveDate.start$gt': '2019'}))
    {'.effectiveDate.start$gt': ['2019']}

    >>> dict(SQ(_lastUpdated__gt=2019))
    {'_lastUpdated': ['gt2019']}

    >>> dict(SQ(Raw(**{'_has:Person:link:id': 'id'})))
    {'_has:Person:link:id': ['id']}

    """
    param_ops = [
        "contains",
        "exact",
        "missing",
        "not",
        "below",
        "above",
        "in",
        "not_in",
        "text",
        "of_type",
    ]
    value_ops = ["eq", "ne", "gt", "ge", "lt", "le", "sa", "eb", "ap"]

    res = defaultdict(list)
    for key, value in kwargs.items():
        value = value if isinstance(value, list) else [value]
        value = [transform_value(sub_value) for sub_value in value]

        key_parts = key.split("__")

        op = None
        if key_parts[-1] in value_ops or key_parts[-1] in param_ops:
            # The operator is always the last part,
            # e.g., birth_date__ge or patient__Patient__birth_date__ge
            op = key_parts[-1]
            key_parts = key_parts[:-1]

        param = key_parts[0]
        for part in key_parts[1:]:
            # Resource type always starts with upper first letter
            is_resource_type = part[0].isupper()

            param += ":" if is_resource_type else "."
            param += part

        if op:
            if op in param_ops:
                param = "{0}:{1}".format(param, transform_param(op))
            elif op in value_ops:
                value = ["{0}{1}".format(op, sub_value) for sub_value in value]
        res[transform_param(param)].extend(value)

    for arg in args:
        if isinstance(arg, Raw):
            for key, value in arg.kwargs.items():
                value = value if isinstance(value, list) else [value]
                res[key].extend(value)
        else:
            raise ValueError("Can't handle args without Raw() wrapper")

    return res


class AbstractSearchSet(ABC):
    client = None
    resource_type = None
    params = None

    def __init__(self, client, resource_type, params=None):
        self.client = client
        self.resource_type = resource_type
        self.params = defaultdict(list, params or {})

    def _dict_to_resource(self, data):
        return self.client.resource(data['resourceType'], **data)

    @abstractmethod  # pragma: no cover
    def fetch(self):
        pass

    @abstractmethod  # pragma: no cover
    def fetch_raw(self):
        pass

    @abstractmethod  # pragma: no cover
    def fetch_all(self):
        pass

    @abstractmethod  # pragma: no cover
    def get(self, id):
        pass

    @abstractmethod  # pragma: no cover
    def count(self):
        pass

    @abstractmethod  # pragma: no cover
    def first(self):
        pass

    @abstractmethod
    async def get_or_create(self, resource):
        pass

    @abstractmethod
    def update(self, resource):
        pass

    @abstractmethod
    def patch(self, resource):
        pass

    def clone(self, override=False, **kwargs):
        new_params = copy.deepcopy(self.params)
        for key, value in kwargs.items():
            if not isinstance(value, list):
                value = [value]

            if override:
                new_params[key] = value
            else:
                new_params[key].extend(value)

        return self.__class__(self.client, self.resource_type, new_params)

    def elements(self, *attrs, exclude=False):
        attrs = set(attrs)
        if not exclude:
            attrs |= {"id", "resourceType"}
        attrs = [attr for attr in attrs]

        return self.clone(
            _elements="{0}{1}".format("-" if exclude else "", ",".join(attrs)),
            override=True,
        )

    def has(self, *args, **kwargs):
        if len(args) % 2 != 0:
            raise TypeError(
                "You should pass even size of arguments, for example: "
                "`.has('Observation', 'patient', "
                "'AuditEvent', 'entity', user='id')`"
            )

        key_part = ":".join(
            ["_has:{0}".format(":".join(pair)) for pair in chunks(args, 2)]
        )

        return self.clone(
            **{":".join([key_part, key]): value for key, value in SQ(**kwargs).items()}
        )

    def include(
        self,
        resource_type,
        attr=None,
        target_resource_type=None,
        *,
        recursive=False,
        iterate=False,
        reverse=False
    ):
        key_params = ["_revinclude" if reverse else "_include"]

        if iterate:
            # Added in FHIR v3.5
            key_params.append("iterate")
        if recursive:
            # Works for FHIR v3.0-3.3
            key_params.append("recursive")
        key = ":".join(key_params)

        if resource_type == "*":
            value = "*"
        else:
            if not attr:
                raise TypeError(
                    "You should provide attr " "(search parameter) argument"
                )
            value_params = [resource_type, attr]
            if target_resource_type:
                value_params.append(target_resource_type)
            value = ":".join(value_params)

        return self.clone(**{key: value})

    def revinclude(
        self,
        resource_type,
        attr=None,
        target_resource_type=None,
        *,
        recursive=False,
        iterate=False
    ):
        return self.include(
            resource_type,
            attr=attr,
            target_resource_type=target_resource_type,
            recursive=recursive,
            iterate=iterate,
            reverse=True,
        )

    def search(self, *args, **kwargs):
        return self.clone(**SQ(*args, **kwargs))

    def limit(self, limit):
        return self.clone(_count=limit, override=True)

    def sort(self, *keys):
        sort_keys = ",".join(keys)
        return self.clone(_sort=sort_keys, override=True)

    def __str__(self):  # pragma: no cover
        return "<{0} {1}?{2}>".format(
            self.__class__.__name__, self.resource_type, encode_params(self.params)
        )

    def __repr__(self):  # pragma: no cover
        return self.__str__()

    def _get_bundle_resources(self, bundle_data):
        bundle_resource_type = bundle_data.get("resourceType", None)

        if bundle_resource_type != "Bundle":
            raise InvalidResponse(
                "Expected to receive Bundle "
                "but {0} received".format(bundle_resource_type)
            )

        resources_data = [res["resource"] for res in bundle_data.get("entry", [])]

        resources = []
        for data in resources_data:
            resource = self._dict_to_resource(data)
            if resource.resource_type == self.resource_type:
                resources.append(resource)
        return resources
