import configparser
import os
import time
from typing import Any, Dict, Final, List, Optional, Tuple

CONFIG_FILE_PATH_DEFAULT: Final[str] = "~/.opik.config"


def create_uuid7():
    ns = time.time_ns()
    last = [0, 0, 0, 0]

    # Simple uuid7 implementation
    sixteen_secs = 16_000_000_000
    t1, rest1 = divmod(ns, sixteen_secs)
    t2, rest2 = divmod(rest1 << 16, sixteen_secs)
    t3, _ = divmod(rest2 << 12, sixteen_secs)
    t3 |= 7 << 12  # Put uuid version in top 4 bits, which are 0 in t3

    # The next two bytes are an int (t4) with two bits for
    # the variant 2 and a 14 bit sequence counter which increments
    # if the time is unchanged.
    if t1 == last[0] and t2 == last[1] and t3 == last[2]:
        # Stop the seq counter wrapping past 0x3FFF.
        # This won't happen in practice, but if it does,
        # uuids after the 16383rd with that same timestamp
        # will not longer be correctly ordered but
        # are still unique due to the 6 random bytes.
        if last[3] < 0x3FFF:
            last[3] += 1
    else:
        last[:] = (t1, t2, t3, 0)
    t4 = (2 << 14) | last[3]  # Put variant 0b10 in top two bits

    # Six random bytes for the lower part of the uuid
    rand = os.urandom(6)
    return f"{t1:>08x}-{t2:>04x}-{t3:>04x}-{t4:>04x}-{rand.hex()}"


def _read_opik_config_file() -> Dict[str, str]:
    config_path = os.path.expanduser(CONFIG_FILE_PATH_DEFAULT)

    config = configparser.ConfigParser()
    config.read(config_path)

    config_values = {
        section: dict(config.items(section)) for section in config.sections()
    }

    if "opik" in config_values:
        return config_values["opik"]

    return {}


def _get_env_variable(key: str) -> Optional[str]:
    env_prefix = "opik_"
    return os.getenv((env_prefix + key).upper(), None)


def get_opik_config_variable(
    key: str, user_value: Optional[str] = None, default_value: Optional[str] = None
) -> Optional[str]:
    """
    Get the configuration value of a variable, order priority is:
    1. user provided value
    2. environment variable
    3. Opik configuration file
    4. default value
    """
    # Return user provided value if it is not None
    if user_value is not None:
        return user_value

    # Return environment variable if it is not None
    env_value = _get_env_variable(key)
    if env_value is not None:
        return env_value

    # Return value from Opik configuration file if it is not None
    config_values = _read_opik_config_file()

    if key in config_values:
        return config_values[key]

    # Return default value if it is not None
    return default_value


def create_usage_object(usage):
    usage_dict = {}

    if usage.completion_tokens is not None:
        usage_dict["completion_tokens"] = usage.completion_tokens
    if usage.prompt_tokens is not None:
        usage_dict["prompt_tokens"] = usage.prompt_tokens
    if usage.total_tokens is not None:
        usage_dict["total_tokens"] = usage.total_tokens
    return usage_dict


def _remove_nulls(x: Dict[str, Any]) -> Dict[str, Any]:
    """Remove None values from dict."""
    return {k: v for k, v in x.items() if v is not None}


def get_traces_and_spans_from_payload(
    payload: List[Dict[str, Any]]
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
    """
    Separate traces and spans from payload.

    Traces are identified by not having a "type" field.
    Spans are identified by having a "type" field.

    Args:
        payload: List of dicts containing trace and span data

    Returns:
        Tuple of (traces, spans) where both are lists of dicts with null values removed
    """
    traces = [_remove_nulls(x) for x in payload if "type" not in x]
    spans = [_remove_nulls(x) for x in payload if "type" in x]
    return traces, spans
