# Mypy does not understand the pydantic classes that allows both alias and name to be used in population
# https://github.com/pydantic/pydantic/issues/3923
# mypy: disable-error-code="call-arg"
from __future__ import annotations

import hashlib
import json
import logging
import re
from typing import Any

from pydantic.alias_generators import to_snake

import cognite.powerops.client._generated.data_classes as data_classes
from cognite.powerops.client._generated.data_classes._core import DomainModelWrite

logger = logging.getLogger("resync")


def get_prefix_from_type(domain_model_type: type) -> str:
    """Get the type prefix for a domain model type.

    Given the domain model type, this function returns the type prefix as a snake case string. Also strips the "Write"
    suffix from the type name to get the root type name.

    Args:
        domain_model_type: The domain model type to get the prefix for.

    Returns:
        The type prefix as a snake case string.
    """

    if issubclass(domain_model_type, data_classes.DomainModelWrite):
        type_name = domain_model_type.__name__.removesuffix("Write")
    else:
        # TODO: should check if it's in the non write classes
        type_name = domain_model_type.__name__
        logger.warning(f"Type {type_name} is not a subclass of DomainModelWrite")

    # Convert the type name to snake case
    external_id_prefix = to_snake(type_name)
    return external_id_prefix


def get_type_prefix_from_string(type_string: str) -> str:
    """Get the type prefix from a string.

    Given a string, this function returns the type prefix as a snake case string. The function converts the string to
    snake case and returns it.

    Args:
        type_string: The string to get the type prefix for.

    Returns:
        The type prefix as a snake case string.
    """

    # Convert the type name to snake case
    type_prefix = to_snake(type_string)
    return type_prefix


# TODO: clean up remove?
def get_data_model_write_classes(data_model_client: Any) -> dict[str, type]:
    """Get all domain model write classes.

    This function returns a dictionary with the type prefix as the key and the domain model write class as the value
    for all domain model write classes.

    Args:
        data_model_client: The pygen data model client to get the domain model write classes from.

    Returns:
        A dictionary with the type prefix as the key and the domain model write class as the value.
    """
    all_data_model_classes = data_classes.__dict__

    expected_types_mapping = {}
    for dm_class in all_data_model_classes.keys():
        if (
            dm_class.endswith("Write")
            and dm_class != "DomainModelWrite"
            and issubclass(all_data_model_classes[dm_class], DomainModelWrite)
        ):
            read_name = to_snake(dm_class.removesuffix("Write"))
            expected_types_mapping[read_name] = all_data_model_classes[dm_class]

    return expected_types_mapping


def ext_id_factory(domain_model_type: type, data: dict) -> str:
    """Generate an external id based on type and data input.

    Given a domain model type and a dictionary of data, this function generates an external id based on the type
    and data input. The external id is generated by combining the type prefix with a cleaned snake case version of
    the name field in the data. Certain types have special handling for external id generation, and if not the name
    field is required and will raise an error if missing.

    Args:
        domain_model_type: The domain model type to generate an external id for.
        data: The data to generate the external id from.

    Returns:
        The generated external id string in snake case format.

    Raises:
        ValueError: If the name field is missing in the data input.
    """
    # TODO: get better external ids
    type_prefix = get_prefix_from_type(domain_model_type)

    if "external_id" in data:
        return data["external_id"]

    # TODO proper naming
    if domain_model_type is data_classes.GeneratorEfficiencyCurveWrite:
        return f"{type_prefix}:{_get_short_hash(data)}"
    elif domain_model_type is data_classes.TurbineEfficiencyCurveWrite:
        return f"{type_prefix}:{_get_short_hash(data)}"

    try:
        name = data["name"]
    except KeyError as exc:
        raise ValueError(f"Missing required `name` field for data type {type_prefix}, {data}") from exc

    cleaned_name = name.lower().replace(" ", "_").replace("-", "_")

    return f"{type_prefix}_{cleaned_name}"


def _get_short_hash(data: dict) -> str:
    dict_string = json.dumps(data, sort_keys=True)
    hash_object = hashlib.sha256(dict_string.encode())
    return hash_object.hexdigest()[:8]


def get_external_id_from_field(
    key: str,
    value: str,
    all_domain_type_properties_types: dict[str, str],
    all_write_classes: dict[str, type],
) -> str | None:
    """Get the external id for a field based on the input value being a reference.

    Given a key and value pair, this function checks if the value is a reference to another object and generates an
    external id based on the reference. The reference can be a direct external id, a reference to another object by
    name, or a reference to another object by name and type. If the value is a reference, the function generates an
    external id based on the reference and returns it. If the value is not a reference, the function returns None.

    Args:
        key: The key of the field in the data input.
        value: The value of the field in the data input.
        all_domain_type_properties_types: A dictionary mapping domain model type properties to their types.
        all_write_classes: A dictionary mapping type prefixes to their domain model types.

    Returns:
        The referenced external id if the value is a reference, otherwise None.

    Raises:
        ValueError: Invalid external id reference.
    """

    if value.startswith("[external_id]"):
        return value.replace("[external_id]", "")
    elif value.startswith("[name]"):
        name_reference = value.replace("[name]", "")
        expected_value_type = all_domain_type_properties_types[key]
        reference_type = get_property_type_from_annotation_string(expected_value_type)
        return ext_id_factory(reference_type, {"name": name_reference})
    elif value.startswith("[name|type:"):
        pattern = r"\[name\|type:(.*?)\](.*)"
        match = re.match(pattern, value)
        if match:
            type_prefix = get_type_prefix_from_string(match.group(1))
            reference_type = all_write_classes[type_prefix]
            name_reference = match.group(2).strip()
        else:
            raise ValueError(f"Invalid external id reference {value}")
        return ext_id_factory(reference_type, {"name": name_reference})

    return None


def get_property_type_from_annotation_string(annotation: str) -> type:
    """Get the domain model type from a string.

    Provided a string, this function returns the domain model write type corresponding to the string. The string is
    expected to be a type prefix, and the function returns the domain model type corresponding to the type prefix. If
    the type prefix is not supported, the function raises a KeyError.

    Args:
        type_string: The type prefix string to get the domain model type for.

    Returns:
        The domain model write type corresponding to the type prefix.

    Raises:
        ValueError: Type is not supported, add import to type
        ValueError: Invalid property for type reference
    """

    regex_pattern = r"\b(\w*Write)\b"
    matches = re.findall(regex_pattern, annotation)

    if matches:
        try:
            type_string = matches[0]
            if not type_string.endswith("Write"):
                type_string += "Write"
            return data_classes.__dict__[type_string]
        except KeyError as exc:
            raise ValueError(f"Type {matches[0]} is not supported, add import to type") from exc
    else:
        raise ValueError(f"Invalid property for type reference {annotation}")


def check_input_keys(data: dict, all_domain_type_properties: list[str]) -> None:
    """Check that all keys in the input data are valid domain model properties.

    Given a dictionary of data, this function checks that all keys in the data are valid domain model properties. If a
    key is not a valid domain model property, the function raises a ValueError.

    Args:
        data: The input data to check.
        all_domain_type_properties: A list of all valid domain model properties.

    Raises:
        ValueError: Key not in domain model properties.
    """

    for key in data.keys():
        if key not in all_domain_type_properties:
            raise ValueError(f"Key {key} not in domain model properties")


def parse_external_ids(
    data: dict,
    all_domain_type_properties_types: dict[str, str],
    all_write_classes: dict[str, type],
) -> tuple[dict, list[str]]:
    """Parse external id references from the input data.

    Given a dictionary of data, this function parses external id references from the data. The function iterates over
    all key-value pairs in the data and checks if the value is a reference to another object. If the value is a
    reference, the function generates an external id based on the reference and replaces the value with the generated
    external id. The function returns the updated data and a list of all external ids referenced in the data.

    Args:
        data: The input data to parse external id references from.
        all_domain_type_properties_types: A dictionary mapping domain model type properties to their types.
        all_write_classes: A dictionary mapping type prefixes to their domain model types.

    Returns:
        data: The updated data with external id references replaced.
        reference_external_ids: A list of all external ids referenced in the data.
    """

    reference_external_ids = []

    for key, value in data.items():
        if isinstance(value, str):
            external_id_reference = get_external_id_from_field(
                key, value, all_domain_type_properties_types, all_write_classes
            )
            if external_id_reference:
                data[key] = external_id_reference
                reference_external_ids.append(external_id_reference)
        elif isinstance(value, list):
            sub_data = []
            for sub_value in value:
                if isinstance(sub_value, str):
                    external_id_reference = get_external_id_from_field(
                        key, sub_value, all_domain_type_properties_types, all_write_classes
                    )
                    if external_id_reference:
                        sub_data.append(external_id_reference)
                        reference_external_ids.append(external_id_reference)
                    else:
                        sub_data.append(sub_value)
                else:
                    # TODO: can this be a mix of external ids and other data?
                    sub_data.append(sub_value)
            data[key] = sub_data

    return data, reference_external_ids
