import threading
from typing import Any
from typing import Dict
from typing import Optional
from typing import TYPE_CHECKING
from typing import Text
from typing import Union

from opentracing import Span as OpenTracingSpan
from opentracing.ext import tags as OTTags

from ddtrace.context import Context as DatadogContext
from ddtrace.ext import errors
from ddtrace.internal.compat import NumericType
from ddtrace.span import Span as DatadogSpan

from .span_context import SpanContext
from .tags import Tags


if TYPE_CHECKING:
    from .tracer import Tracer


_TagNameType = Union[Text, bytes]


class Span(OpenTracingSpan):
    """Datadog implementation of :class:`opentracing.Span`"""

    def __init__(self, tracer, context, operation_name):
        # type: (Tracer, Optional[SpanContext], str) -> None
        if context is not None:
            context = SpanContext(ddcontext=context._dd_context, baggage=context.baggage)
        else:
            context = SpanContext()

        super(Span, self).__init__(tracer, context)

        self.finished = False
        self._lock = threading.Lock()
        # use a datadog span
        self._dd_span = DatadogSpan(tracer._dd_tracer, operation_name, context=context._dd_context)

    def finish(self, finish_time=None):
        # type: (Optional[float]) -> None
        """Finish the span.

        This calls finish on the ddspan.

        :param finish_time: specify a custom finish time with a unix timestamp
            per time.time()
        :type timestamp: float
        """
        if self.finished:
            return

        # finish the datadog span
        self._dd_span.finish(finish_time)
        self.finished = True

    def set_baggage_item(self, key, value):
        # type: (str, Any) -> Span
        """Sets a baggage item in the span context of this span.

        Baggage is used to propagate state between spans.

        :param key: baggage item key
        :type key: str

        :param value: baggage item value
        :type value: a type that can be compat.stringify()'d

        :rtype: Span
        :return: itself for chaining calls
        """
        new_ctx = self.context.with_baggage_item(key, value)
        with self._lock:
            self._context = new_ctx
        return self

    def get_baggage_item(self, key):
        # type: (str) -> Optional[str]
        """Gets a baggage item from the span context of this span.

        :param key: baggage item key
        :type key: str

        :rtype: str
        :return: the baggage value for the given key or ``None``.
        """
        return self.context.get_baggage_item(key)

    def set_operation_name(self, operation_name):
        # type: (str) -> None
        """Set the operation name."""
        self._dd_span.name = operation_name

    def log_kv(self, key_values, timestamp=None):
        # type: (Dict[_TagNameType, Any], Optional[float]) -> Span
        """Add a log record to this span.

        Passes on relevant opentracing key values onto the datadog span.

        :param key_values: a dict of string keys and values of any type
        :type key_values: dict

        :param timestamp: a unix timestamp per time.time()
        :type timestamp: float

        :return: the span itself, for call chaining
        :rtype: Span
        """

        # match opentracing defined keys to datadog functionality
        # opentracing/specification/blob/1be630515dafd4d2a468d083300900f89f28e24d/semantic_conventions.md#log-fields-table
        for key, val in key_values.items():
            if key == "event" and val == "error":
                # TODO: not sure if it's actually necessary to set the error manually
                self._dd_span.error = 1
                self.set_tag("error", 1)
            elif key == "error" or key == "error.object":
                self.set_tag(errors.ERROR_TYPE, val)
            elif key == "message":
                self.set_tag(errors.ERROR_MSG, val)
            elif key == "stack":
                self.set_tag(errors.ERROR_STACK, val)
            else:
                pass

        return self

    def set_tag(self, key, value):
        # type: (_TagNameType, Any) -> None
        """Set a tag on the span.

        This sets the tag on the underlying datadog span.
        """
        if key == Tags.SPAN_TYPE:
            self._dd_span.span_type = value
        elif key == Tags.SERVICE_NAME:
            self._dd_span.service = value
        elif key == Tags.RESOURCE_NAME or key == OTTags.DATABASE_STATEMENT:
            self._dd_span.resource = value
        elif key == OTTags.PEER_HOSTNAME:
            self._dd_span.set_tag(Tags.TARGET_HOST, value)
        elif key == OTTags.PEER_PORT:
            self._dd_span.set_tag(Tags.TARGET_PORT, value)
        elif key == Tags.SAMPLING_PRIORITY:
            self._dd_span.context.sampling_priority = value
        else:
            self._dd_span.set_tag(key, value)

    def _get_tag(self, key):
        # type: (_TagNameType) -> Optional[Text]
        """Gets a tag from the span.

        This method retrieves the tag from the underlying datadog span.
        """
        return self._dd_span.get_tag(key)

    def _get_metric(self, key):
        # type: (_TagNameType) -> Optional[NumericType]
        """Gets a metric from the span.

        This method retrieves the metric from the underlying datadog span.
        """
        return self._dd_span.get_metric(key)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            self._dd_span.set_exc_info(exc_type, exc_val, exc_tb)

        # note: self.finish() AND _dd_span.__exit__ will call _span.finish() but
        # it is idempotent
        self._dd_span.__exit__(exc_type, exc_val, exc_tb)
        self.finish()

    def _associate_dd_span(self, ddspan):
        # type: (DatadogSpan) -> None
        """Associates a DD span with this span."""
        # get the datadog span context
        self._dd_span = ddspan
        self.context._dd_context = ddspan.context

    @property
    def _dd_context(self):
        # type: () -> DatadogContext
        return self._dd_span.context
