import json
from typing import Optional


class TimestreamDimensions:
    """
    TimestreamDimensions is a class wrapping an underlying dictionary of Timestream dimensions.
    The underlying dictionary simply maps dimensions keys (strings) to dimensions values (strings).

    These objects may optionally be initialized with a dimensions dictionary, and the underlying dict
    can be updated with the "add_dimension" and "remove_dimension" functions. This class controls
    validation of the initial dimensions dictionary and all updates to that dictionary.

    This class also includes utilities for converting itself into the appropriate structure needed
    for a Timestream write, converting itself into a human friendly dict or string, and implements
    equality and a hash string functions for quick comparisons of Timestream dimensions.
    """

    def __init__(self, dimensions: Optional[dict] = None):
        assert (
            not dimensions or type(dimensions) is dict
        ), f'Invalid type for initializing TimestreamDimensions "{type(dimensions)}"'
        assert not dimensions or all(
            isinstance(k, str) and isinstance(v, str) for k, v in dimensions.items()
        ), f"All keys and values of dimensions must be strings"
        self.__dimensions = {}
        for k, v in (dimensions or {}).items():
            self.add_dimension(k, v)

    def add_dimension(self, key: str, value: str, overwrite: bool = False):
        assert key and type(key) is str, f'Cannot add dimension {key} with type "{type(key)}"'
        assert type(value) is str, f'Cannot add dimension value {value} with type "{type(value)}"'
        assert (
            overwrite or key not in self.__dimensions
        ), f"Dimension {key}:{value} already exists and cannot be added to dimensions {self}"
        self.__dimensions[key.strip()] = value.strip()

    def remove_dimension(self, key: str):
        assert key in self.__dimensions, f"Cannot remove dimension {key} from {self}"
        self.__dimensions.pop(key)

    def get_dimension(self, key: str) -> str:
        return self.__dimensions.get(key)

    def get_dimension_keys(self) -> list[str]:
        return list(self.__dimensions.keys())

    def loadable(self, omit_dimensions: Optional[list[str]] = None) -> list[dict]:
        """loadable returns a list of dictionaries that is ready to load into Timestream"""
        omit_dimensions = omit_dimensions or []
        assert isinstance(omit_dimensions, list)
        return [
            {"Name": k, "Value": v, "DimensionValueType": "VARCHAR"}
            for k, v in self.__dimensions.items()
            if k not in omit_dimensions
        ]

    def to_dict(self) -> dict:
        return self.__dimensions.copy()

    def copy(self):
        return TimestreamDimensions(self.__dimensions)

    def hashstr(self) -> str:
        """
        Provides a hash string for this TimestreamDimensions object.

        A TimestreamDimensions object is defined by its underlying dict of dimension
        keys to dimension values (irrespective of key order in the dict).

        The hash string is created by converting the underlying dict into a list of key-value
        pairs (e.g. key="vpd", value="9.1" yields "vpd‽‽‽9.1"), sorting that list alphabetically,
        and converting the list into a string using the JSON encoder.
        """
        hash_separator = "‽‽‽"
        hash_lst = [f"{k}{hash_separator}{v}" for k, v in self.__dimensions.items()]
        hash_lst.sort()
        return json.dumps(hash_lst)

    def __eq__(self, other):
        return type(self) == type(other) and self.hashstr() == other.hashstr()

    def __repr__(self) -> str:
        return json.dumps(self.to_dict())

    def __len__(self) -> int:
        return len(self.__dimensions)
