"""Dynamische Inventory-Operationen für Questra Data."""

from __future__ import annotations

from datetime import date, datetime, time
from typing import Any
from uuid import UUID

from loguru import logger


def _normalize_name(name: str) -> str:
    """
    Normalisiert Namen für GraphQL Field Names.

    Konvertiert Namen wie 'TestNamespace' zu 'testNamespace'
    (nur erste Zeichen lowercase).

    Args:
        name: Der zu normalisierende Name

    Returns:
        str: Normalisierter Name
    """
    if not name:
        return name
    return name[0].lower() + name[1:]


def _convert_numbers_to_strings(value: Any) -> Any:
    """
    Konvertiert Zahlen und Datetime-Typen rekursiv in Strings für GraphQL API.

    Hintergrund: JavaScript hat Limitierungen bei großen Zahlen (2^53-1),
    daher erwartet die GraphQL API Zahlen als Strings (IntNumberString).
    Datetime-Objekte werden zu ISO-8601 Strings serialisiert.

    Args:
        value: Beliebiger Wert (int, float, datetime, dict, list, etc.)

    Returns:
        Wert mit konvertierten Zahlen und Datetime-Objekten
    """
    # WICHTIG: bool ist eine Subklasse von int in Python, daher MUSS bool
    # VORHER geprüft werden, sonst werden True/False zu "1"/"0" konvertiert!
    if isinstance(value, bool):
        # Booleans unverändert lassen
        return value
    elif isinstance(value, datetime):
        # datetime zu ISO-8601 String mit Timezone
        return value.isoformat()
    elif isinstance(value, date):
        # date zu ISO-8601 String (YYYY-MM-DD)
        return value.isoformat()
    elif isinstance(value, time):
        # time zu ISO-8601 String (HH:MM:SS oder HH:MM:SS.ffffff)
        return value.isoformat()
    elif isinstance(value, UUID):
        # UUID zu String
        return str(value)
    elif isinstance(value, (int, float)):
        # Konvertiere Zahlen zu Strings
        return str(value)
    elif isinstance(value, dict):
        # Rekursiv für Dictionaries
        return {key: _convert_numbers_to_strings(val) for key, val in value.items()}
    elif isinstance(value, list):
        # Rekursiv für Listen
        return [_convert_numbers_to_strings(item) for item in value]
    else:
        # Alle anderen Typen (str, None) unverändert
        return value


def _normalize_items_names(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """
    Normalisiert die Schlüssel in einer Liste von Items und konvertiert Zahlen zu Strings.

    Args:
        items: Liste von Items mit beliebigen Schlüssel-Namen

    Returns:
        Liste von Items mit normalisierten Schlüssel-Namen und Zahlen als Strings
    """
    return [
        {
            _normalize_name(key): _convert_numbers_to_strings(value)
            for key, value in item.items()
        }
        for item in items
    ]


def _build_nested_field_selection(property_name: str) -> str:
    """
    Konvertiert Punkt-Notation einer Property in geschachtelte GraphQL-Field-Syntax.

    Args:
        property_name: Property-Name mit optionaler Punkt-Notation (z.B. "BodyBattery.id" oder "Name")

    Returns:
        GraphQL-Field-Selection-String

    Beispiele:
        "Name" -> "name"
        "BodyBattery.id" -> "bodyBattery { id }"
        "BodyBattery.interval.timeUnit" -> "bodyBattery { interval { timeUnit } }"
    """
    parts = property_name.split(".")
    if len(parts) == 1:
        # Einfache Property ohne Verschachtelung
        return _normalize_name(parts[0])

    # Verschachtelte Property - baue rekursiv GraphQL fields auf
    normalized_parts = [_normalize_name(p) for p in parts]
    result = normalized_parts[0]
    for part in normalized_parts[1:]:
        result = f"{result} {{ {part}"
    result += " }" * (len(parts) - 1)
    return result


class DynamicInventoryOperations:
    """
    Dynamische Operationen für Inventory-Daten.

    Nach dem Erstellen eines Inventorys (z.B. 'TestUser' im Namespace 'TestNamespace')
    werden automatisch folgende GraphQL-Operationen generiert:

    Queries:
    - testNamespace__TestUser: Abfragen von Items mit Pagination

    Mutations:
    - testNamespace__insertTestUser: Items erstellen
    - testNamespace__updateTestUser: Items aktualisieren
    - testNamespace__deleteTestUser: Items löschen

    Diese Klasse generiert automatisch die passenden GraphQL-Queries und Mutations
    und validiert Eingaben gegen das GraphQL-Schema.

    Methoden folgen Python CRUD-Namenskonventionen:
    - create(): Neue Items erstellen
    - list(): Items auflisten/abfragen
    - update(): Bestehende Items aktualisieren
    - delete(): Items löschen
    """

    def __init__(self, execute_func: callable):
        """
        Initialisiert die dynamischen Inventory-Operationen.

        Args:
            execute_func: Funktion zum Ausführen von GraphQL-Operationen
        """
        self._execute = execute_func
        self._schema_cache: dict[str, dict[str, Any]] = {}
        logger.debug("DynamicInventoryOperations initialized")

    def _check_inventory_exists(
        self, inventory_name: str, namespace_name: str | None = None
    ) -> bool:
        """
        Prüft ob ein Inventory existiert.

        Args:
            inventory_name: Name des Inventorys
            namespace_name: Optionaler Namespace

        Returns:
            bool: True wenn Inventory existiert, sonst False
        """
        try:
            # Query um Inventory zu prüfen (ohne Retries)
            query = """
                query CheckInventory(
                    $where: _InventoryFilter__InputType
                ) {
                    _inventories(where: $where) {
                        name
                        namespace {
                            name
                        }
                    }
                }
            """

            variables = {
                "where": {
                    "inventoryNames": [inventory_name],
                    "namespaceNames": ([namespace_name] if namespace_name else None),
                }
            }

            # Keine Retries bei Validierung (retries=0)
            result = self._execute(query, variables, retries=0)
            inventories = result.get("_inventories", [])

            return len(inventories) > 0

        except Exception as e:
            logger.warning(f"Failed to check inventory existence: {e}")
            return True  # Optimistisch fortfahren

    def _load_inventory_schema(
        self, inventory_name: str, namespace_name: str | None = None
    ) -> dict[str, Any]:
        """
        Lädt das Schema für ein bestimmtes Inventory vom Server.

        Args:
            inventory_name: Name des Inventorys
            namespace_name: Optionaler Namespace

        Returns:
            dict: Schema-Informationen (fields, required_fields, etc.)

        Raises:
            ValueError: Wenn Inventory nicht existiert
        """
        cache_key = f"{namespace_name or ''}::{inventory_name}"

        if cache_key in self._schema_cache:
            logger.debug(f"Using cached schema for {cache_key}")
            return self._schema_cache[cache_key]

        logger.debug(f"Loading schema for {cache_key} from server")

        # Prüfe ob Inventory existiert
        if not self._check_inventory_exists(inventory_name, namespace_name):
            ns_part = f" in namespace '{namespace_name}'" if namespace_name else ""
            raise ValueError(f"Inventory '{inventory_name}'{ns_part} does not exist")

        type_name = self._build_type_name(inventory_name, namespace_name)

        # Introspection Query für einen bestimmten Type
        introspection_query = f"""
            query IntrospectType {{
                __type(name: "{type_name}__InsertInputType") {{
                    name
                    inputFields {{
                        name
                        type {{
                            kind
                            name
                            ofType {{
                                kind
                                name
                                ofType {{
                                    kind
                                    name
                                }}
                            }}
                        }}
                    }}
                }}
            }}
        """

        try:
            # Keine Retries bei Schema-Introspection (retries=0)
            result = self._execute(introspection_query, retries=0)
            type_info = result.get("__type")

            if not type_info:
                raise ValueError(
                    f"Schema for '{type_name}__InsertInputType' not found. "
                    f"Inventory may not be properly created."
                )

            # Extrahiere Feld-Informationen
            fields = {}
            required_fields = set()

            for field in type_info.get("inputFields", []):
                field_name = field["name"]
                field_type = field["type"]

                # Prüfe ob Feld required ist (NON_NULL)
                is_required = field_type.get("kind") == "NON_NULL"
                is_array = False
                base_type = None

                # Navigiere durch die Type-Struktur
                current_type = field_type
                while current_type:
                    if current_type.get("kind") == "LIST":
                        is_array = True
                    elif current_type.get("kind") in ["SCALAR", "ENUM", "INPUT_OBJECT"]:
                        base_type = current_type.get("name")
                        break
                    current_type = current_type.get("ofType")

                fields[field_name] = {
                    "required": is_required,
                    "array": is_array,
                    "type": base_type,
                }

                if is_required:
                    required_fields.add(field_name)

            schema_info = {"fields": fields, "required_fields": required_fields}

            self._schema_cache[cache_key] = schema_info
            logger.info(
                f"Loaded schema for {cache_key}: "
                f"{len(fields)} fields, {len(required_fields)} required"
            )

            return schema_info

        except ValueError:
            raise
        except Exception as e:
            raise ValueError(f"Failed to load schema for {type_name}: {e}")

    def _validate_items(
        self,
        items: list[dict[str, Any]],
        inventory_name: str,
        namespace_name: str | None = None,
        operation: str = "insert",
    ) -> None:
        """
        Validiert Items gegen das Schema.

        Feldnamen sind case-insensitive für den ersten Buchstaben
        (Name == name, Email == email).

        Args:
            items: Liste von Items zum Validieren
            inventory_name: Name des Inventorys
            namespace_name: Optionaler Namespace
            operation: Operation (insert, update, delete)

        Raises:
            ValueError: Wenn Validierung fehlschlägt
        """
        if operation == "insert":
            schema_info = self._load_inventory_schema(inventory_name, namespace_name)

            required_fields = schema_info.get("required_fields", set())
            available_fields = schema_info.get("fields", {})

            # Erstelle Mappings mit normalisiertem ersten Buchstaben
            required_fields_normalized = {_normalize_name(f) for f in required_fields}
            available_fields_normalized = {
                _normalize_name(f) for f in available_fields.keys()
            }

            for idx, item in enumerate(items):
                # Normalisiere Item-Keys (erster Buchstabe lowercase)
                item_keys_normalized = {_normalize_name(k) for k in item.keys()}

                # Prüfe required fields
                missing_fields = required_fields_normalized - item_keys_normalized
                if missing_fields:
                    raise ValueError(
                        f"Item {idx}: Missing required fields: "
                        f"{', '.join(sorted(missing_fields))}"
                    )

                # Prüfe unbekannte Felder
                if available_fields:
                    unknown_fields = item_keys_normalized - available_fields_normalized
                    if unknown_fields:
                        logger.warning(
                            f"Item {idx}: Unknown fields will be ignored: "
                            f"{', '.join(sorted(unknown_fields))}"
                        )

        elif operation in ["update", "delete"]:
            # Für Update und Delete: _id und _rowVersion sind Pflicht
            for idx, item in enumerate(items):
                if "_id" not in item:
                    raise ValueError(
                        f"Item {idx}: Missing required field '_id' "
                        f"for {operation} operation"
                    )
                if "_rowVersion" not in item:
                    raise ValueError(
                        f"Item {idx}: Missing required field '_rowVersion' "
                        f"for {operation} operation"
                    )

    def _build_field_name(
        self,
        inventory_name: str,
        namespace_name: str | None = None,
        operation: str | None = None,
    ) -> str:
        """
        Erstellt den GraphQL Field Name aus Namespace und Inventory.

        Args:
            inventory_name: Name des Inventorys (z.B. 'TestUser')
            namespace_name: Optionaler Namespace (z.B. 'TestNamespace')
            operation: Optionale Operation (z.B. 'insert', 'update', 'delete')

        Returns:
            str: Field Name (z.B. 'testNamespace__TestUser' oder 'testNamespace__insertTestUser')
        """
        if namespace_name:
            normalized_ns = _normalize_name(namespace_name)
            base_name = f"{normalized_ns}__{inventory_name}"
        else:
            base_name = _normalize_name(inventory_name)

        if operation:
            # Für Operationen: testNamespace__insertTestUser
            if namespace_name:
                normalized_ns = _normalize_name(namespace_name)
                return f"{normalized_ns}__{operation}{inventory_name}"
            else:
                return f"{operation}{inventory_name}"

        return base_name

    def _build_type_name(
        self,
        inventory_name: str,
        namespace_name: str | None = None,
    ) -> str:
        """
        Erstellt den GraphQL Type Name aus Namespace und Inventory.

        Args:
            inventory_name: Name des Inventorys (z.B. 'TestUser')
            namespace_name: Optionaler Namespace (z.B. 'TestNamespace')

        Returns:
            str: Type Name (z.B. 'testNamespace__TestUser')
        """
        if namespace_name:
            normalized_ns = _normalize_name(namespace_name)
            return f"{normalized_ns}__{inventory_name}"
        else:
            return inventory_name

    def list(
        self,
        inventory_name: str,
        namespace_name: str | None = None,
        properties: list[str] | None = None,
        where: dict[str, Any] | None = None,
        order: list[dict[str, Any]] | None = None,
        first: int | None = None,
        after: str | None = None,
        last: int | None = None,
        before: str | None = None,
    ) -> dict[str, Any]:
        """
        Listet Items aus einem dynamischen Inventory auf.

        Args:
            inventory_name: Name des Inventorys (z.B. 'TestUser')
            namespace_name: Optionaler Namespace (z.B. 'TestNamespace')
            properties: Properties die abgefragt werden sollen (Standard: ['_id', '_rowVersion'])
                       Unterstützt Punkt-Notation für verschachtelte Properties:
                       - Einfach: 'Name', 'Email'
                       - Verschachtelt: 'BodyBattery.id', 'BodyBattery.interval.timeUnit'
            where: Filter-Bedingungen
            order: Sortierung
            first: Anzahl Elemente vom Anfang
            after: Cursor für Pagination
            last: Anzahl Elemente vom Ende
            before: Cursor für Pagination

        Returns:
            dict: Dictionary mit pageInfo und nodes/edges

        Beispiele:
            # Einfache Properties
            result = client.inventory.list(
                inventory_name='TestUser',
                namespace_name='TestNamespace',
                properties=['_id', 'Name', 'Email', 'Age'],
                first=10
            )

            # Verschachtelte Properties (z.B. TimeSeries)
            result = client.inventory.list(
                inventory_name='TestUser',
                namespace_name='TestNamespace',
                properties=['_id', 'Name', 'BodyBattery.id', 'BodyBattery.unit', 'BodyBattery.interval.timeUnit'],
                first=10
            )
        """
        field_name = self._build_field_name(inventory_name, namespace_name)
        type_name = self._build_type_name(inventory_name, namespace_name)

        # Standard-Properties wenn nichts angegeben
        if not properties:
            properties = ["_id", "_rowVersion"]

        # Baue verschachtelte Feldauswahl für jede Property (intern GraphQL fields)
        normalized_fields = [_build_nested_field_selection(prop) for prop in properties]
        fields_str = "\n                    ".join(normalized_fields)

        logger.debug(
            "Listing items from dynamic inventory",
            field_name=field_name,
            type_name=type_name,
            properties=properties,
            where=where,
            order=order,
        )

        # Baue GraphQL Query
        query = f"""
            query ListInventory(
                $where: {type_name}__FilterInputType
                $order: [{type_name}__SortInputType!]
                $first: Int
                $after: String
                $last: Int
                $before: String
            ) {{
                {field_name}(
                    where: $where
                    order: $order
                    first: $first
                    after: $after
                    last: $last
                    before: $before
                ) {{
                    pageInfo {{
                        hasNextPage
                        hasPreviousPage
                        startCursor
                        endCursor
                    }}
                    nodes {{
                        {fields_str}
                    }}
                }}
            }}
        """

        variables = {
            "where": where,
            "order": order,
            "first": first,
            "after": after,
            "last": last,
            "before": before,
        }

        result = self._execute(query, variables)
        data = result[field_name]

        logger.info(
            f"Listed {len(data.get('nodes', []))} items from {field_name}",
            has_next_page=data.get("pageInfo", {}).get("hasNextPage"),
        )

        return data

    def create(
        self,
        inventory_name: str,
        items: list[dict[str, Any]],
        namespace_name: str | None = None,
        validate: bool = True,
    ) -> list[dict[str, Any]]:
        """
        Erstellt neue Items in einem dynamischen Inventory.

        Args:
            inventory_name: Name des Inventorys (z.B. 'TestUser')
            items: Liste von Items zum Erstellen
            namespace_name: Optionaler Namespace (z.B. 'TestNamespace')
            validate: Schema-Validierung durchführen (Standard: True)

        Returns:
            list[dict]: Liste der erstellten Items mit _id, _rowVersion, _existed
                (Nur diese 3 Standard-Felder werden zurückgegeben)

        Raises:
            ValueError: Wenn Validierung fehlschlägt

        Examples:
            ```python
            items = client.inventory.create(
                inventory_name="TestUser",
                namespace_name="TestNamespace",
                items=[
                    {
                        "Name": "John Doe",
                        "Email": "john@example.com",
                        "Age": 30,
                        "IsActive": True,
                    },
                    {
                        "Name": "Jane Doe",
                        "Email": "jane@example.com",
                        "Age": 28,
                        "IsActive": True,
                    },
                ],
            )
            ```
        """
        # Validierung
        if validate:
            self._validate_items(items, inventory_name, namespace_name, "insert")

        field_name = self._build_field_name(inventory_name, namespace_name, "insert")

        type_name = self._build_type_name(inventory_name, namespace_name)

        logger.debug(
            "Creating items in dynamic inventory",
            field_name=field_name,
            type_name=type_name,
            num_items=len(items),
            validated=validate,
        )

        # Der Return-Typ ist _Items__PayloadType mit items Array
        mutation = f"""
            mutation CreateInventory(
                $input: [{type_name}__InsertInputType!]!
            ) {{
                {field_name}(input: $input) {{
                    items {{
                        _id
                        _rowVersion
                        _existed
                    }}
                }}
            }}
        """

        normalized_items = _normalize_items_names(items)

        variables = {"input": normalized_items}

        result = self._execute(mutation, variables)
        created_items = result[field_name]["items"]

        logger.info(
            f"Created {len(created_items)} items in {field_name}",
            num_new=sum(1 for item in created_items if not item.get("_existed", False)),
        )

        return created_items

    def update(
        self,
        inventory_name: str,
        items: list[dict[str, Any]],
        namespace_name: str | None = None,
        validate: bool = True,
    ) -> list[dict[str, Any]]:
        """
        Aktualisiert Items in einem dynamischen Inventory.

        Args:
            inventory_name: Name des Inventorys (z.B. 'TestUser')
            items: Liste von Items zum Aktualisieren (mit _id, _rowVersion)
            namespace_name: Optionaler Namespace (z.B. 'TestNamespace')
            validate: Schema-Validierung durchführen (Standard: True)

        Returns:
            list[dict]: Liste der aktualisierten Items mit _id, _rowVersion, _existed
                (Nur diese 3 Standard-Felder werden zurückgegeben)

        Raises:
            ValueError: Wenn Validierung fehlschlägt

        Examples:
            ```python
            items = client.inventory.update(
                inventory_name="TestUser",
                namespace_name="TestNamespace",
                items=[{"_id": "123", "_rowVersion": "1", "Age": 31}],
            )
            ```
        """
        # Validierung
        if validate:
            self._validate_items(items, inventory_name, namespace_name, "update")

        field_name = self._build_field_name(inventory_name, namespace_name, "update")
        type_name = self._build_type_name(inventory_name, namespace_name)

        logger.debug(
            "Updating items in dynamic inventory",
            field_name=field_name,
            type_name=type_name,
            num_items=len(items),
            validated=validate,
        )

        # Der Return-Typ ist _Items__PayloadType mit items Array
        mutation = f"""
            mutation UpdateInventory(
                $input: [{type_name}__UpdateInputType!]!
            ) {{
                {field_name}(input: $input) {{
                    items {{
                        _id
                        _rowVersion
                        _existed
                    }}
                }}
            }}
        """

        normalized_items = _normalize_items_names(items)

        variables = {"input": normalized_items}

        result = self._execute(mutation, variables)
        updated_items = result[field_name]["items"]

        logger.info(
            f"Updated {len(updated_items)} items in {field_name}",
            num_found=sum(1 for item in updated_items if item.get("_existed", False)),
        )

        return updated_items

    def delete(
        self,
        inventory_name: str,
        items: list[dict[str, Any]],
        namespace_name: str | None = None,
        validate: bool = True,
    ) -> list[dict[str, Any]]:
        """
        Löscht Items aus einem dynamischen Inventory.

        Args:
            inventory_name: Name des Inventorys (z.B. 'TestUser')
            items: Liste von Items zum Löschen (mit _id und _rowVersion)
            namespace_name: Optionaler Namespace (z.B. 'TestNamespace')
            validate: Schema-Validierung durchführen (Standard: True)

        Returns:
            list[dict]: Liste mit _id, _rowVersion, _existed für jedes gelöschte Item
                (Nur diese 3 Standard-Felder werden zurückgegeben)

        Raises:
            ValueError: Wenn Validierung fehlschlägt

        Examples:
            ```python
            items = client.inventory.delete(
                inventory_name="TestUser",
                namespace_name="TestNamespace",
                items=[{"_id": "123", "_rowVersion": "1"}],
            )
            ```
        """
        # Validierung
        if validate:
            self._validate_items(items, inventory_name, namespace_name, "delete")

        field_name = self._build_field_name(inventory_name, namespace_name, "delete")
        type_name = self._build_type_name(inventory_name, namespace_name)

        logger.debug(
            "Deleting items from dynamic inventory",
            field_name=field_name,
            type_name=type_name,
            num_items=len(items),
            validated=validate,
        )

        # Der Return-Typ ist _Items__PayloadType mit items Array
        mutation = f"""
            mutation DeleteInventory(
                $input: [{type_name}__DeleteInputType!]!
            ) {{
                {field_name}(input: $input) {{
                    items {{
                        _id
                        _rowVersion
                        _existed
                    }}
                }}
            }}
        """

        variables = {"input": items}

        result = self._execute(mutation, variables)
        deleted_items = result[field_name]["items"]

        logger.info(
            f"Deleted {len(deleted_items)} items from {field_name}",
            num_found=sum(1 for item in deleted_items if item.get("_existed", False)),
        )

        return deleted_items
