from contextvars import ContextVar
from datetime import datetime
from types import TracebackType
from typing import Any, Dict, Generic, List, Literal, Optional, Type, TypeVar, cast
from uuid import uuid4

from ..constants import DEVELOPMENT_TAG_NAME, ONLINE_TAG_NAME, PRODUCTION_TAG_NAME
from ..context import get_context
from ..views import Model, Trace

T = TypeVar("T")


class TracingContext(Generic[T]):
    def __init__(self, function_name: str, do_not_persist_traces: bool) -> None:
        self._do_not_persist_traces = do_not_persist_traces
        self._models: List[Model] = []
        self._values: Dict[str, Any] = {}
        self._trace: Optional[Trace[T]] = None
        self._start_time = datetime.utcnow()
        self._name = function_name

    def log_value(self, name: str, value: Any) -> None:
        self._values[name] = value

    def log_model(self, model: Model) -> None:
        self._models.append(model)

    def finalise(
        self, output: Optional[T] = None, exception: Optional[BaseException] = None
    ) -> Trace[T]:
        assert self._trace is None, "has been already finalised"

        delta_time = (datetime.utcnow() - self._start_time).microseconds / 1000
        self._trace = cast(
            Trace[T],
            Trace(  # avoid ValueError: "Trace" object has no field "__orig_class__"
                trace_id=str(uuid4()),
                created=self._start_time.isoformat(),
                original_execution_time_ms=delta_time,
                logged_values=self._values,
                models=self._models,
                output=output,
                exception=None
                if exception is None
                else f"{type(exception).__name__}: {exception}",
                tags=[
                    self._name,
                    ONLINE_TAG_NAME,
                    PRODUCTION_TAG_NAME
                    if get_context().is_production
                    else DEVELOPMENT_TAG_NAME,
                ],
            ),
        )

        return self._trace

    @staticmethod
    def get_current_tracing_context() -> Optional["TracingContext"]:
        return _current_tracing_context.get()

    def __enter__(self) -> "TracingContext":
        _current_tracing_context.set(self)
        return self

    def __exit__(
        self,
        type: Optional[Type[BaseException]],
        exception: Optional[BaseException],
        traceback: Optional[TracebackType],
    ) -> Literal[False]:
        _current_tracing_context.set(None)

        if exception is not None and type is not None:
            self.finalise(exception=exception)
            if get_context().should_log_exception_stack:
                get_context().logger.exception("Could not finish operation")
            else:
                get_context().logger.error(
                    f"Could not finish operation because of {type.__name__}: {exception}"
                )

        assert self._trace is not None
        if not self._do_not_persist_traces:
            get_context().tracing_database.save(self._trace)

        return False


_current_tracing_context: ContextVar[Optional[TracingContext]] = ContextVar(
    "_current_tracing_context"
)
