from dataclasses import dataclass
from enum import auto
from typing import Optional, TYPE_CHECKING, Union

from mstrio.utils.enum_helper import AutoName
from mstrio.utils.exceptions import NotSupportedError
from mstrio.utils.helper import Dictable, exception_handler

if TYPE_CHECKING:
    from mstrio.connection import Connection
    from mstrio.modeling.schema import UserHierarchy
    from mstrio.modeling.schema.attribute import Attribute


class ObjectSubType(AutoName):
    """String literal used to identify the type of a metadata object.
    Some MicroStrategy APIs make a distinction between an object's type
    and subtype because in some cases (e.g. the different kinds of reports)
    it can be useful to see one "report" concept rather than to have to always
    distinguish between different kinds of reports. But there are cases
    (e.g. filters versus custom-groups or users versus user-groups ) where
    users want to see the different subtypes of the same type as fundamentally
    different.
    Used across different modeling modules e.g. attributes, user hierarchies.
    """

    FILTER = auto()
    CUSTOM_GROUP = auto()
    FILTER_PARTITION = auto()
    SEGMENT = auto()
    TEMPLATE = auto()
    REPORT_GRID = auto()
    REPORT_GRAPH = auto()
    REPORT_ENGINE = auto()
    REPORT_TEXT = auto()
    REPORT_DATAMART = auto()
    REPORT_BASE = auto()
    REPORT_GRID_AND_GRAPH = auto()
    REPORT_NON_INTERACTIVE = auto()
    REPORT_CUBE = auto()
    REPORT_INCREMENT_REFRESH = auto()
    REPORT_TRANSACTION = auto()
    REPORT_EMMA_CUBE = auto()
    REPORT_EMMA_CUBE_IRR = auto()
    REPORT_HYPER_CARD = auto()
    METRIC = auto()
    METRIC_SUBTOTAL = auto()
    SYSTEM_SUBTOTAL = auto()
    METRIC_DMX = auto()
    METRIC_TRAINING = auto()
    METRIC_EXTREME = auto()
    METRIC_REFERENCE_LINE = auto()
    METRIC_RELATIONSHIP = auto()
    STYLE = auto()
    AGG_METRIC = auto()
    FOLDER = auto()
    FOLDER_SYSTEM = auto()
    DEVICE = auto()
    PROMPT = auto()
    PROMPT_BOOLEAN = auto()
    PROMPT_LONG = auto()
    PROMPT_STRING = auto()
    PROMPT_DOUBLE = auto()
    PROMPT_DATE = auto()
    PROMPT_OBJECTS = auto()
    PROMPT_ELEMENTS = auto()
    PROMPT_EXPRESSION = auto()
    PROMPT_EXPRESSION_DRAFT = auto()
    PROMPT_DIMTY = auto()
    PROMPT_BIG_DECIMAL = auto()
    FUNCTION = auto()
    FUNCTION_THIRD_PARTY = auto()
    ATTRIBUTE = auto()
    ATTRIBUTE_ROLE = auto()
    ATTRIBUTE_TRANSFORMATION = auto()
    ATTRIBUTE_ABSTRACT = auto()
    ATTRIBUTE_RECURSIVE = auto()
    ATTRIBUTE_DERIVED = auto()
    FACT = auto()
    DIMENSION_SYSTEM = auto()
    DIMENSION_USER = auto()
    DIMENSION_ORDERED = auto()
    DIMENSION_USER_HIERARCHY = auto()
    LOGICAL_TABLE = auto()
    TABLE_PARTITION_MD = auto()
    TABLE_PARTITION_WH = auto()
    DATAMART_REPORT = auto()
    FACT_GROUP = auto()
    SHORTCUT = auto()
    SHORTCUT_WEAK_REF = auto()
    RESOLUTION = auto()
    ATTRIBUTE_FORM_SYSTEM = auto()
    ATTRIBUTE_FORM_NORMAL = auto()
    ATTRIBUTE_FORM_CUSTOM = auto()
    SCHEMA = auto()
    FORMAT = auto()
    CATALOG = auto()
    CATALOG_DEFN = auto()
    COLUMN = auto()
    COLUMN_NORMAL = auto()
    COLUMN_CUSTOM = auto()
    PROPERTY_GROUP = auto()
    PROPERTY_SET = auto()
    DB_ROLE = auto()
    DB_ROLE_IMPORT = auto()
    DB_ROLE_IMPORT_PRIMARY = auto()
    DB_ROLE_O_AUTH = auto()
    DB_ROLE_REMOTE_DATE_SOURCE = auto()
    DB_ROLE_URL_AUTH = auto()
    DB_ROLE_GENERIC_DATA_CONNECTOR = auto()
    DB_ROLE_DATA_IMPORT_SERVER = auto()
    DB_ROLE_CLOUD_ELEMENT = auto()
    DB_LOGIN = auto()
    DB_CONNECTION = auto()
    PROJECT = auto()
    SERVER_DEF = auto()
    USER = auto()
    USER_GROUP = auto()
    TRANSMITTER = auto()
    CONFIGURATION = auto()
    REQUEST = auto()
    SEARCH = auto()
    INDEXED_SEARCH = auto()
    RELATIONSHIP_SEARCH = auto()
    SEARCH_FOLDER = auto()
    SEARCH_FOLDER_CROSS_PROJECT = auto()
    DATAMART = auto()
    FUNCTION_PACKAGE_DEFINITION = auto()
    ROLE = auto()
    ROLE_TRANSFORMATION = auto()
    SECURITY_ROLE = auto()
    LOCALE = auto()
    CONSOLIDATION = auto()
    CONSOLIDATION_DERIVED = auto()
    CONSOLIDATION_ELEMENT = auto()
    SCHEDULE_EVENT = auto()
    SCHEDULE_OBJECT = auto()
    SCHEDULE_TRIGGER = auto()
    LINK = auto()
    PHYSICAL_TABLE = auto()
    DB_TABLE_PMT = auto()
    DB_TABLE_SOURCE = auto()
    DOCUMENT_DEFINITION = auto()
    REPORT_WRITING_DOCUMENT = auto()
    DOCUMENT_THEME = auto()
    DOSSIER = auto()
    DRILL_MAP = auto()
    DBMS = auto()
    MD_SECURITY_FILTER = auto()
    MONITOR_PERFORMANCE = auto()
    MONITOR_JOBS = auto()
    MONITOR_USER_CONNECTIONS = auto()
    MONITOR_DB_CONNECTIONS = auto()
    PROMPT_ANSWER = auto()
    PROMPT_ANSWER_BOOLEAN = auto()
    PROMPT_ANSWER_LONG = auto()
    PROMPT_ANSWER_STRING = auto()
    PROMPT_ANSWER_DOUBLE = auto()
    PROMPT_ANSWER_DATE = auto()
    PROMPT_ANSWER_OBJECTS = auto()
    PROMPT_ANSWER_ELEMENTS = auto()
    PROMPT_ANSWER_EXPRESSION = auto()
    PROMPT_ANSWER_EXPRESSION_DRAFT = auto()
    PROMPT_ANSWER_DIMTY = auto()
    PROMPT_ANSWER_BIG_DECIMAL = auto()
    PROMPT_ANSWER_INT64 = auto()
    PROMPT_ANSWERS = auto()
    GRAPH_STYLE = auto()
    CHANGE_JOURNAL_SEARCH = auto()
    BLOB_UNKNOWN = auto()
    BLOB_OTHER = auto()
    BLOB_IMAGE = auto()
    BLOB_PROJECT_PACKAGE = auto()
    BLOB_EXCEL = auto()
    BLOB_HTML_TEMPLATE = auto()
    DASHBOARD_TEMPLATE = auto()
    FLAG = auto()
    CHANGE_JOURNAL = auto()
    EXTERNAL_SHORTCUT_UNKNOWN = auto()
    EXTERNAL_SHORTCUT_URL = auto()
    EXTERNAL_SHORTCUT_SNAPSHOT = auto()
    EXTERNAL_SHORTCUT_TARGET = auto()
    RECONCILIATION = auto()
    PALETTE_SYSTEM = auto()
    PALETTE_CUSTOM = auto()
    THRESHOLDS = auto()
    SUBSCRIPTION_ADDRESS = auto()
    SUBSCRIPTION_CONTACT = auto()
    SUBSCRIPTION_INSTANCE = auto()


class TableColumnMergeOption(AutoName):
    REUSE_ANY = auto()
    REUSE_COMPATIBLE_DATA_TYPE = auto()
    REUSE_MATCHED_DATA_TYPE = auto()


class TablePrefixOption(AutoName):
    ADD_DEFAULT_PREFIX = auto()
    ADD_NAMESPACE = auto()


class PhysicalTableType(AutoName):
    NORMAL = auto()
    WAREHOUSE_PARTITION = auto()
    SQL = auto()
    RESERVED = auto()


@dataclass(eq=False)
class SchemaObjectReference(Dictable):
    """Information about an object referenced within the specification
    of another object. An object reference typically contains only enough
    fields to uniquely identify the referenced objects.

    Attributes:
        object_id: object's id, a globally unique identifier used to
            distinguish between metadata objects within the same project
        sub_type:  string literal used to identify the type of a metadata object
        name: the name of the metadata object
        is_embedded: if true indicates that the target object of this reference
            is embedded within this object, bool
    """

    _FROM_DICT_MAP = {'sub_type': ObjectSubType}

    sub_type: ObjectSubType
    object_id: Optional[str] = None
    name: Optional[str] = None
    is_embedded: Optional[bool] = None

    def __eq__(self, other):
        if not isinstance(other, SchemaObjectReference):
            return False

        return self.object_id == other.object_id and self.sub_type == other.sub_type

    def __hash__(self):
        return hash(self.object_id)

    @classmethod
    def create_from(
        cls,
        schema_object: Union["Attribute", "UserHierarchy"],
        is_embedded: bool = None,
    ) -> "SchemaObjectReference":
        """Converts a schema object into a schema object reference

        Args:
            schema_object: a schema object
            is_embeded: a boolean indicating whether the schema object
                is embedded or not

        Returns:
            SchemaObjectReference of the given schema object
        """
        reference_body = {
            "object_id": schema_object.id,
            "name": schema_object.name,
            "sub_type": schema_object.sub_type,
            "is_embeded": is_embedded,
        }
        return cls.from_dict(reference_body)

    def to_object(
        self, connection: "Connection"
    ) -> Union["Attribute", "UserHierarchy"]:
        """Converts a schema object reference into a schema object.

        Args:
            connection: a connection object required to fetch
                the schema object
        """
        if self.sub_type == ObjectSubType.ATTRIBUTE:
            from mstrio.modeling.schema.attribute import Attribute

            return Attribute(connection, id=self.object_id)
        else:
            raise NotSupportedError(
                f"{self.sub_type} object sub type is not supported."
            )


class DataType(Dictable):
    """Representation in the object model for a data-type that could be used
    for a SQL column.

    Attributes:
        type: gross data type of an actual or proposed column in a database.
        precision: for relevant data types, the length of the representation
        scale: for relevant data types, the fixed position used
        in the representation
    """

    class Type(AutoName):
        """String literal used to identify the gross data type of an actual
        or proposed column in a database."""

        UNKNOWN = auto()
        RESERVED = auto()
        INTEGER = auto()
        UNSIGNED = auto()
        NUMERIC = auto()
        DECIMAL = auto()
        REAL = auto()
        FLOAT = auto()
        CHAR = auto()
        FIXED_LENGTH_STRING = auto()
        VARIABLE_LENGTH_STRING = auto()
        BINARY = auto()
        VAR_BIN = auto()
        LONGVARBIN = auto()
        DATE = auto()
        TIME = auto()
        TIME_STAMP = auto()
        N_CHAR = auto()
        N_VAR_CHAR = auto()
        SHORT = auto()
        LONG = auto()
        MB_CHAR = auto()
        BOOL = auto()
        PATTERN = auto()
        N_PATTERN = auto()
        CELL_FORMAT_DATA = auto()
        MISSING = auto()
        UTF8_CHAR = auto()
        INT64 = auto()
        GUID = auto()
        DOUBLE_DOUBLE = auto()

    def __init__(self, type: Type, precision: str, scale: str) -> None:
        self.type = type
        self.precision = precision
        self.scale = scale


class FormReference(Dictable):
    """The reference that identifies a form object within the context of a
    given attribute. When writing back an attribute, either id or name is
    needed to identify a form, and if both are provided, id will take
    the higher priority.

    Attributes:
        id: id of the form
        name: name of the form
    """

    def __init__(self, id: str = None, name: str = None) -> None:
        if id is None and name is None:
            exception_handler(
                "Provide either `id` or `name` of a form object.", AttributeError
            )
        self.id = id
        self.name = name


class AttributeDisplays(Dictable):
    """The collections of report displays and browse displays of the attribute.

    Attributes:
        report_displays: list of an AttributeSorts for report displays
        browse_displays: list of an AttributeSorts for browse displays
    """

    _FROM_DICT_MAP = {
        "report_displays": (
            lambda source, connection: [
                FormReference.from_dict(content, connection) for content in source
            ]
        ),
        "browse_displays": (
            lambda source, connection: [
                FormReference.from_dict(content, connection) for content in source
            ]
        ),
    }

    def __init__(
        self, report_displays: list[FormReference], browse_displays: list[FormReference]
    ) -> None:
        self.report_displays = report_displays
        self.browse_displays = browse_displays


class AttributeSort(Dictable):
    """An individual sort element in an AttributeSorts list.

    Attributes:
        form: A form reference
        ascending: whether the sort is in ascending or descending order
    """

    _FROM_DICT_MAP = {"form": FormReference.from_dict}

    def __init__(self, form: FormReference, ascending: bool = False) -> None:
        self.form = form
        self.ascending = ascending


class AttributeSorts(Dictable):
    """The collections of report sorts and browse sorts of the attribute.

    Attributes:
        report_sorts: list of an AttributeSorts for report sorts
        browse_sorts: list of an AttributeSorts for browse sorts
    """

    _FROM_DICT_MAP = {
        "report_sorts": (
            lambda source, connection: [
                AttributeSort.from_dict(content, connection) for content in source
            ]
        ),
        "browse_sorts": (
            lambda source, connection: [
                AttributeSort.from_dict(content, connection) for content in source
            ]
        ),
    }

    def __init__(
        self,
        report_sorts: Optional[list[AttributeSort]] = None,
        browse_sorts: Optional[list[AttributeSort]] = None,
    ) -> None:
        self.report_sorts = report_sorts
        self.browse_sorts = browse_sorts


@dataclass
class TableColumn(Dictable):
    """An object representation of a physical column that might
    appear in some data source. In addition to representing physical
    columns, we also use this object to represent columns that do not
    actually appear in any data source but which the engine should
    create if it needs to make a column to contain data for some higher
    level construct (e.g. a fact, an attribute form etc.)."""

    _FROM_DICT_MAP = {"data_type": DataType, "sub_type": ObjectSubType}
    data_type: DataType
    column_name: Optional[str] = None  # When retrieved as part of a logical tab
    name: Optional[str] = None  # When retrieved from datasources API
    id: Optional[str] = None
    sub_type: Optional[ObjectSubType] = None
    date_created: Optional[str] = None
    date_modified: Optional[str] = None
    version_id: Optional[str] = None
    primary_locale: Optional[str] = None

    @classmethod
    def from_dict(cls, source, connection, to_snake_case=True) -> 'TableColumn':
        source = source.copy()

        if information := source.get('information', None):
            source.update(information)
            source['id'] = source.pop('objectId')

        return super().from_dict(source, connection, to_snake_case)
