"""
EN: CSV data manager for the BBW University Research Platform backend.
    - Loads and saves object data from/to CSV files using type hints.
DE: CSV-Datenmanager für das Backend der Forschungsplattform der bbw Hochschule.
    - Lädt und speichert Objektdaten aus/in CSV-Dateien unter Verwendung von Typ-Hinweisen.
"""

# EN: Import necessary modules: 'csv' for file operations, 'logging' for messages, 'pathlib' for modern path handling,
#     and 'typing' for advanced type hints.
# DE: Notwendige Module importieren: 'csv' für Dateioperationen, 'logging' für Meldungen, 'pathlib' für modernes Pfad-Handling
#     und 'typing' für erweiterte Typ-Hinweise.
import csv
import logging
from pathlib import Path
from typing import Any, List, Type, get_type_hints, Union

# EN: Import custom exception for specific error handling.
# DE: Importieren der benutzerdefinierten Ausnahme für spezifische Fehlerbehandlung.
from app.ausnahmen.ausnahmeverwaltung import CSVVerarbeitungsFehler


class DatenManager:
    """
    DE: Eine Utility-Klasse mit statischen Methoden zum Laden/Speichern von Objektdaten aus/in CSV-Dateien.
    EN: A utility class with static methods for loading/saving object data from/to CSV files.
    """

    @staticmethod
    def lade_aus_csv(datei_pfad: str, objekt_typ: Type) -> List[Any]:
        """
        DE: Lädt Daten aus einer CSV-Datei und wandelt jede Zeile in ein Objekt des angegebenen Typs um.
            Nutzt Type-Hints aus dem __init__ der Klasse, um die Spaltenwerte korrekt zu konvertieren.
        EN: Loads data from a CSV file and converts each row into an object of the specified type.
            It uses type hints from the class's __init__ to correctly convert column values.
        """
        daten = []
        path_obj = Path(datei_pfad)

        # EN: Check if the file exists before attempting to open it.
        # DE: Überprüfen, ob die Datei existiert, bevor versucht wird, sie zu öffnen.
        if not path_obj.exists():
            raise CSVVerarbeitungsFehler(
                f"Datei nicht gefunden: {datei_pfad}",
                f"File not found: {datei_pfad}"
            )

        try:
            # EN: Get the type hints from the constructor of the target class (e.g., {'name': <class 'str'>, 'price': <class 'float'>}).
            # DE: Die Typ-Hinweise aus dem Konstruktor der Zielklasse abrufen (z.B. {'name': <class 'str'>, 'price': <class 'float'>}).
            type_hints = get_type_hints(objekt_typ.__init__)
            with path_obj.open(mode='r', encoding='utf-8', newline='') as f:
                reader = csv.reader(f)
                header = next(reader)  # EN: Read the header row. / DE: Kopfzeile lesen.

                # EN: Iterate over each data row in the CSV.
                # DE: Über jede Datenzeile in der CSV-Datei iterieren.
                for i, zeile in enumerate(reader):
                    # EN: Create a dictionary mapping header names to raw string values from the row.
                    # DE: Ein Wörterbuch erstellen, das Header-Namen den rohen Zeichenkettenwerten aus der Zeile zuordnet.
                    raw_kwargs = dict(zip(header, zeile))
                    typed_kwargs = {}

                    try:
                        # EN: Iterate over the raw data to convert each value to its target type.
                        # DE: Über die Rohdaten iterieren, um jeden Wert in seinen Zieltyp umzuwandeln.
                        for key, value in raw_kwargs.items():
                            if key in type_hints:
                                target_type = type_hints[key]
                                # EN: Check for complex types like Union (used for Optional).
                                # DE: Auf komplexe Typen wie Union (verwendet für Optional) prüfen.
                                origin = getattr(target_type, '__origin__', None)
                                args = getattr(target_type, '__args__', ())

                                # EN: Logic for optional types (e.g., Union[int, None]).
                                # DE: Logik für optionale Typen (z.B. Union[int, None]).
                                if origin is Union and type(None) in args:
                                    if value == '':
                                        typed_kwargs[key] = None
                                    else:
                                        # EN: Get the actual type from the Union (e.g., int from Union[int, None]).
                                        # DE: Den tatsächlichen Typ aus der Union erhalten (z.B. int aus Union[int, None]).
                                        actual_type = next(t for t in args if t is not type(None))
                                        typed_kwargs[key] = actual_type(value)
                                else:
                                    # EN: Normal conversion for simple types (str, int, float).
                                    # DE: Normale Konvertierung für einfache Typen (str, int, float).
                                    typed_kwargs[key] = target_type(value)
                            else:
                                # EN: If no type hint is found for a column, keep it as a string.
                                # DE: Wenn kein Typ-Hinweis für eine Spalte gefunden wird, als Zeichenkette belassen.
                                typed_kwargs[key] = value

                        # EN: Use the new dictionary with correctly typed values to create the object instance.
                        # DE: Das neue Wörterbuch mit korrekt typisierten Werten verwenden, um die Objektinstanz zu erstellen.
                        daten.append(objekt_typ(**typed_kwargs))

                    except (ValueError, TypeError) as e:
                        # EN: If a row fails to process (e.g., "abc" cannot be converted to float), log a warning and skip it.
                        # DE: Wenn eine Zeile nicht verarbeitet werden kann (z.B. "abc" kann nicht in float umgewandelt werden), eine Warnung protokollieren und überspringen.
                        logging.warning(
                            f"[CSV-Warnung] Zeile {i + 2} in '{datei_pfad}' konnte nicht verarbeitet werden: {e}."
                        )
                        continue

        except (IOError, StopIteration, KeyError) as e:
            # EN: Catch broader errors related to file reading or format issues (e.g., empty file, missing column).
            # DE: Breitere Fehler im Zusammenhang mit dem Lesen von Dateien oder Formatproblemen abfangen (z.B. leere Datei, fehlende Spalte).
            raise CSVVerarbeitungsFehler(
                f"Fehler beim Lesen der CSV-Datei {datei_pfad}: {e}",
                f"Error reading CSV file {datei_pfad}: {e}"
            )

        return daten

    @staticmethod
    def speichere_in_csv(datei_pfad: str, daten: List[Any]):
        """
        DE: Speichert eine Liste von Objekten in einer CSV-Datei.
            Die Spaltenüberschriften werden aus den Attributnamen des ersten Objekts abgeleitet.
        EN: Saves a list of objects to a CSV file.
            The column headers are derived from the attribute names of the first object.
        """
        if not daten:
            logging.warning("[CSV-Warnung] Keine Daten zum Speichern vorhanden.")
            return

        # EN: Get the header from the instance variables of the first object in the list.
        # DE: Den Header aus den Instanzvariablen des ersten Objekts in der Liste abrufen.
        header = list(vars(daten[0]).keys())

        try:
            with open(datei_pfad, 'w', encoding='utf-8', newline='') as f:
                writer = csv.writer(f)
                writer.writerow(header)

                # EN: Iterate through the list of objects and write their attribute values as a row.
                # DE: Durch die Liste der Objekte iterieren und ihre Attributwerte als Zeile schreiben.
                for obj in daten:
                    row = [getattr(obj, k) for k in header]
                    writer.writerow(row)

            logging.info(f"[CSV-Info] Daten erfolgreich in {datei_pfad} gespeichert.")

        except (IOError, AttributeError) as e:
            raise CSVVerarbeitungsFehler(
                f"Fehler beim Schreiben der Datei {datei_pfad}: {e}",
                f"Error writing file {datei_pfad}: {e}"
            )