from __future__ import absolute_import

import functools
import six
from collections import defaultdict, Iterable, OrderedDict
from dateutil.parser import parse as parse_datetime
from pytz import UTC

from django.core.cache import cache

from sentry import options
from sentry.api.event_search import FIELD_ALIASES, PROJECT_ALIAS, USER_DISPLAY_ALIAS
from sentry.models import Project
from sentry.api.utils import default_start_end_dates
from sentry.snuba.dataset import Dataset
from sentry.tagstore import TagKeyStatus
from sentry.tagstore.base import TagStorage, TOP_VALUES_DEFAULT_LIMIT
from sentry.tagstore.exceptions import (
    GroupTagKeyNotFound,
    GroupTagValueNotFound,
    TagKeyNotFound,
    TagValueNotFound,
)
from sentry.tagstore.types import TagKey, TagValue, GroupTagKey, GroupTagValue
from sentry.utils import snuba, metrics
from sentry.utils.hashlib import md5_text
from sentry.utils.dates import to_timestamp
from sentry_relay.consts import SPAN_STATUS_CODE_TO_NAME


SEEN_COLUMN = "timestamp"

# columns we want to exclude from methods that return
# all values for a given tag/column
BLACKLISTED_COLUMNS = frozenset(["project_id"])

FUZZY_NUMERIC_KEYS = frozenset(
    ["stack.colno", "stack.in_app", "stack.lineno", "stack.stack_level", "transaction.duration"]
)
FUZZY_NUMERIC_DISTANCE = 50

tag_value_data_transformers = {"first_seen": parse_datetime, "last_seen": parse_datetime}


def fix_tag_value_data(data):
    for key, transformer in tag_value_data_transformers.items():
        if key in data:
            data[key] = transformer(data[key]).replace(tzinfo=UTC)
    return data


def get_project_list(project_id):
    return project_id if isinstance(project_id, Iterable) else [project_id]


class SnubaTagStorage(TagStorage):
    def __get_tag_key(self, project_id, group_id, environment_id, key):
        tag = u"tags[{}]".format(key)
        filters = {"project_id": get_project_list(project_id)}
        if environment_id:
            filters["environment"] = [environment_id]
        if group_id is not None:
            filters["group_id"] = [group_id]
        conditions = [[tag, "!=", ""]]
        aggregations = [["uniq", tag, "values_seen"], ["count()", "", "count"]]

        result = snuba.query(
            conditions=conditions,
            filter_keys=filters,
            aggregations=aggregations,
            referrer="tagstore.__get_tag_key",
        )
        if result is None or result["count"] == 0:
            raise TagKeyNotFound if group_id is None else GroupTagKeyNotFound
        else:
            data = {"key": key, "values_seen": result["values_seen"], "count": result["count"]}
            if group_id is None:
                return TagKey(**data)
            else:
                return GroupTagKey(group_id=group_id, **data)

    def __get_tag_key_and_top_values(
        self, project_id, group_id, environment_id, key, limit=3, raise_on_empty=True, **kwargs
    ):

        tag = u"tags[{}]".format(key)
        filters = {"project_id": get_project_list(project_id)}
        if environment_id:
            filters["environment"] = [environment_id]
        if group_id is not None:
            filters["group_id"] = [group_id]
        conditions = kwargs.get("conditions", [])
        aggregations = kwargs.get("aggregations", [])

        conditions.append([tag, "!=", ""])
        aggregations += [
            ["uniq", tag, "values_seen"],
            ["count()", "", "count"],
            ["min", SEEN_COLUMN, "first_seen"],
            ["max", SEEN_COLUMN, "last_seen"],
        ]

        result, totals = snuba.query(
            start=kwargs.get("start"),
            end=kwargs.get("end"),
            groupby=[tag],
            conditions=conditions,
            filter_keys=filters,
            aggregations=aggregations,
            orderby="-count",
            limit=limit,
            totals=True,
            referrer="tagstore.__get_tag_key_and_top_values",
        )

        if raise_on_empty and (not result or totals.get("count", 0) == 0):
            raise TagKeyNotFound if group_id is None else GroupTagKeyNotFound
        else:
            if group_id is None:
                key_ctor = TagKey
                value_ctor = TagValue
            else:
                key_ctor = functools.partial(GroupTagKey, group_id=group_id)
                value_ctor = functools.partial(GroupTagValue, group_id=group_id)

            top_values = [
                value_ctor(
                    key=key,
                    value=value,
                    times_seen=data["count"],
                    first_seen=parse_datetime(data["first_seen"]),
                    last_seen=parse_datetime(data["last_seen"]),
                )
                for value, data in six.iteritems(result)
            ]

            return key_ctor(
                key=key,
                values_seen=totals.get("values_seen", 0),
                count=totals.get("count", 0),
                top_values=top_values,
            )

    def __get_tag_keys(
        self,
        project_id,
        group_id,
        environment_ids,
        limit=1000,
        keys=None,
        include_values_seen=True,
        **kwargs
    ):
        return self.__get_tag_keys_for_projects(
            get_project_list(project_id),
            group_id,
            environment_ids,
            kwargs.get("start"),
            kwargs.get("end"),
            limit,
            keys,
            include_values_seen=include_values_seen,
        )

    def __get_tag_keys_for_projects(
        self,
        projects,
        group_id,
        environments,
        start,
        end,
        limit=1000,
        keys=None,
        include_values_seen=True,
        use_cache=False,
        **kwargs
    ):
        """ Query snuba for tag keys based on projects

            When use_cache is passed, we'll attempt to use the cache. There's an exception if group_id was passed
            which refines the query enough caching isn't required.
            The cache key is based on the filters being passed so that different queries don't hit the same cache, with
            exceptions for start and end dates. Since even a microsecond passing would result in a different caching
            key, which means always missing the cache.
            Instead, to keep the cache key the same for a short period we append the duration, and the end time rounded
            with a certain jitter to the cache key.
            This jitter is based on the hash of the key before duration/end time is added for consistency per query.
            The jitter's intent is to avoid a dogpile effect of many queries being invalidated at the same time.
            This is done by changing the rounding of the end key to a random offset. See snuba.quantize_time for
            further explanation of how that is done.
        """
        default_start, default_end = default_start_end_dates()
        if start is None:
            start = default_start
        if end is None:
            end = default_end

        filters = {"project_id": sorted(projects)}
        if environments:
            filters["environment"] = sorted(environments)
        if group_id is not None:
            filters["group_id"] = [group_id]
        if keys is not None:
            filters["tags_key"] = sorted(keys)
        aggregations = [["count()", "", "count"]]

        if include_values_seen:
            aggregations.append(["uniq", "tags_value", "values_seen"])
        conditions = []

        should_cache = use_cache and group_id is None
        result = None

        if should_cache:
            filtering_strings = [
                u"{}={}".format(key, value) for key, value in six.iteritems(filters)
            ]
            cache_key = u"tagstore.__get_tag_keys:{}".format(
                md5_text(*filtering_strings).hexdigest()
            )
            key_hash = hash(cache_key)
            should_cache = (key_hash % 1000) / 1000.0 <= options.get(
                "snuba.tagstore.cache-tagkeys-rate"
            )

        # If we want to continue attempting to cache after checking against the cache rate
        if should_cache:
            # Needs to happen before creating the cache suffix otherwise rounding will cause different durations
            duration = (end - start).total_seconds()
            # Cause there's rounding to create this cache suffix, we want to update the query end so results match
            end = snuba.quantize_time(end, key_hash)
            cache_key += u":{}@{}".format(duration, end.isoformat())
            result = cache.get(cache_key, None)
            if result is not None:
                metrics.incr("testing.tagstore.cache_tag_key.hit")
            else:
                metrics.incr("testing.tagstore.cache_tag_key.miss")

        if result is None:
            result = snuba.query(
                start=start,
                end=end,
                groupby=["tags_key"],
                conditions=conditions,
                filter_keys=filters,
                aggregations=aggregations,
                limit=limit,
                orderby="-count",
                referrer="tagstore.__get_tag_keys",
                **kwargs
            )
            if should_cache:
                cache.set(cache_key, result, 300)
                metrics.incr("testing.tagstore.cache_tag_key.len", amount=len(result))

        if group_id is None:
            ctor = TagKey
        else:
            ctor = functools.partial(GroupTagKey, group_id=group_id)

        results = set()
        for key, data in six.iteritems(result):
            params = {"key": key}
            if include_values_seen:
                params["values_seen"] = data["values_seen"]
                params["count"] = data["count"]
            else:
                # If only one aggregate is requested then data is just that raw
                # aggregate value, rather than a dictionary of
                # key:aggregate_value pairs
                params["count"] = data
            results.add(ctor(**params))
        return results

    def __get_tag_value(self, project_id, group_id, environment_id, key, value):
        tag = u"tags[{}]".format(key)
        filters = {"project_id": get_project_list(project_id)}
        if environment_id:
            filters["environment"] = [environment_id]
        if group_id is not None:
            filters["group_id"] = [group_id]
        conditions = [[tag, "=", value]]
        aggregations = [
            ["count()", "", "times_seen"],
            ["min", SEEN_COLUMN, "first_seen"],
            ["max", SEEN_COLUMN, "last_seen"],
        ]

        data = snuba.query(
            conditions=conditions,
            filter_keys=filters,
            aggregations=aggregations,
            referrer="tagstore.__get_tag_value",
        )
        if not data["times_seen"] > 0:
            raise TagValueNotFound if group_id is None else GroupTagValueNotFound
        else:
            data.update({"key": key, "value": value})
            if group_id is None:
                return TagValue(**fix_tag_value_data(data))
            else:
                return GroupTagValue(group_id=group_id, **fix_tag_value_data(data))

    def get_tag_key(self, project_id, environment_id, key, status=TagKeyStatus.VISIBLE, **kwargs):
        assert status is TagKeyStatus.VISIBLE
        return self.__get_tag_key_and_top_values(project_id, None, environment_id, key, **kwargs)

    def get_tag_keys(
        self, project_id, environment_id, status=TagKeyStatus.VISIBLE, include_values_seen=False
    ):
        assert status is TagKeyStatus.VISIBLE
        return self.__get_tag_keys(project_id, None, environment_id and [environment_id])

    def get_tag_keys_for_projects(
        self, projects, environments, start, end, status=TagKeyStatus.VISIBLE, use_cache=False
    ):
        MAX_UNSAMPLED_PROJECTS = 50
        # We want to disable FINAL in the snuba query to reduce load.
        optimize_kwargs = {"turbo": True}
        # If we are fetching less than MAX_UNSAMPLED_PROJECTS, then disable
        # the sampling that turbo enables so that we get more accurate results.
        # We only want sampling when we have a large number of projects, so
        # that we don't cause performance issues for Snuba.
        if len(projects) <= MAX_UNSAMPLED_PROJECTS:
            optimize_kwargs["sample"] = 1
        return self.__get_tag_keys_for_projects(
            projects,
            None,
            environments,
            start,
            end,
            include_values_seen=False,
            use_cache=use_cache,
            **optimize_kwargs
        )

    def get_tag_value(self, project_id, environment_id, key, value):
        return self.__get_tag_value(project_id, None, environment_id, key, value)

    def get_tag_values(self, project_id, environment_id, key):
        key = self.__get_tag_key_and_top_values(