# Databricks notebook source

# Import algemene packagaes
import xxhash
import random
import string

# Import packages voor pyspark
from databricks.sdk.runtime import *
from pyspark.sql.functions import *
from pyspark.sql.types import *
from datetime import datetime
from pyspark.sql import Row, DataFrame
from typing import *

from datateam_moss import spark_io_utils as siu
from datateam_moss.logger import get_logger

logger = get_logger("__datamodelleren__")

def voeg_willekeurig_toe_en_hash_toe(df: DataFrame, business_key: str, naam_id: str):
    """
    Neemt de naam van een kolom als invoer aan, veronderstelt dat het een gehashte waarde bevat,
    voegt aan elke waarde een willekeurig woord of getal toe en maakt een nieuwe hash..

    Parameters:
    - df: DataFrame: Het DataFrame waar je de hash aan wil toevoegen
    - business_key: str: De naam van de invoerkolom.
    - naam_id: str: De naam van de primary key

    Returns:
    - DataFrame: Een DataFrame met de oorspronkelijke gehashte waarde en de nieuwe gehashte waarde.
    """
    def voeg_willekeurig_toe_en_hash_toe_udf(waarde):
        """
        Neemt een waarde, voegt er een willekeurig woord of getal aan toe en maakt een nieuwe hash.

        Parameters:
        - waarde: De invoerwaarde.

        Returns:
        - int: De nieuwe gehashte waarde als integer.
        """
        willekeurig_deel = ''.join(random.choices(string.ascii_letters + string.digits, k=10))

        # Controleer het type van de invoerwaarde en handel elk geval dienovereenkomstig af
        if isinstance(waarde, int):
            waarde_str = str(waarde)
        elif isinstance(waarde, bool):
            waarde_str = str(waarde)
        else:
            waarde_str = str(waarde)

        geconcateneerde_reeks = waarde_str + willekeurig_deel
        nieuwe_hash_waarde = xxhash.xxh64(geconcateneerde_reeks).intdigest()

        # Modulo operation to limit the hash value within the range of LongType
        hash_in_range = nieuwe_hash_waarde % (2**63 - 1)  # Maximum value for LongType
        return hash_in_range

    # Registreer de UDF
    udf_voeg_willekeurig_toe_en_hash_toe = udf(voeg_willekeurig_toe_en_hash_toe_udf, returnType=LongType())
    
    # Pas de UDF toe op het DataFrame
    resultaat_df = (df.withColumn(naam_id, udf_voeg_willekeurig_toe_en_hash_toe(col(business_key))))
    return resultaat_df

#
# Uitfaseren
#

def maak_onbekende_dimensie(df, naam_bk, naam_id="", uitzonderings_kolommen=[]):
    """
    Maakt een nieuwe DataFrame met een record voor ontbrekende waarden in een dimensietabel.

    Args:
        df (DataFrame): Het bron DataFrame.
        naam_bk (str): De naam van de business_key (BK) kolom.
        naam_id (str): De naam van de primaire sleutel / ID kolom.
        uitzonderings_kolommen (list, optional): Lijst van kolommen die moeten worden uitgesloten bij het vergelijken van versies. Standaard is leeg.

    Returns:
        DataFrame: Een nieuwe DataFrame met een record voor ontbrekende waarden.

    Raises:
        ValueError: Als de opgegeven kolomnaam voor de ID niet aanwezig is in het DataFrame.
    """
    nieuwe_data = []

    for col in df.columns:
        col_type = df.schema[col].dataType
        if col == naam_bk:
            nieuwe_data.append("0")
        elif col == naam_id:
            nieuwe_data.append(0)
        elif isinstance(col_type, DoubleType):
            nieuwe_data.append(0.00)
        elif isinstance(col_type, IntegerType) or isinstance(col_type, LongType):
            nieuwe_data.append(0)
        elif isinstance(col_type, StringType):
            nieuwe_data.append(f'N/A')
        elif isinstance(col_type, BooleanType):
            nieuwe_data.append(None)
        elif isinstance(col_type, TimestampType):
            datum_tijd = datetime.strptime("9999-12-31 23:59:59", "%Y-%m-%d %H:%M:%S")
            nieuwe_data.append(datum_tijd)
        elif isinstance(col_type, DateType):
            datum_tijd = datetime.strptime("9999-12-31", "%Y-%m-%d").date()
            nieuwe_data.append(datum_tijd)
        elif isinstance(col_type, NullType):
            nieuwe_data.append('N/A') 

    nieuwe_rij = Row(*nieuwe_data)
    schema = StructType([StructField(col, df.schema[col].dataType, True) for col in df.columns])

    # Bepaal het schema op basis van het dataframe
    onbekende_dimensie = spark.createDataFrame([nieuwe_rij], schema)  # Maak een nieuwe rij
    df_met_onbekende_dimensie = onbekende_dimensie.union(df)
    return df_met_onbekende_dimensie


# Deze functie zal in de toekomst de bovenstaande functie vervangen. Om dit te realiseren moet alle code eerst gebruik maken van deze functie.
def maak_onbekende_ongeldige_dimensie(df, naam_bk, naam_sid="", onbekend_ongeldig="onbekend"):
    """
    Maakt een nieuwe record voor ontbrekende waarden in een dimensietabel.

    Args:
        df (DataFrame): Het bron DataFrame.
        naam_bk (str): De naam van de business_key (BK) kolom.
        naam_sid (str): De naam van de primaire sleutel / ID kolom.
        onbekend_ongeldig: geeft aan of je een onbekende of ongeldig record wilt maken. Standaard is "onbekend".

    Returns:
        DataFrame: Een nieuwe DataFrame met een record voor ontbrekende waarden.

    Raises:
        ValueError: Als de opgegeven kolomnaam voor de ID niet aanwezig is in het DataFrame.
    """
    nieuwe_data = []

    for col in df.columns:
        col_type = df.schema[col].dataType
        if col == naam_bk and onbekend_ongeldig == "onbekend":
            nieuwe_data.append("onbekend")
        elif col == naam_bk and onbekend_ongeldig == "ongeldig":
            nieuwe_data.append("ongeldig")
        elif col == naam_sid and onbekend_ongeldig == "onbekend":
            nieuwe_data.append(-1)
        elif col == naam_sid and onbekend_ongeldig == "ongeldig":
            nieuwe_data.append(-2)
        elif col.lower() in ("m_geldig_van", "geldig_van"):
            nieuwe_data.append(datetime(1900, 1, 1))
        elif col.lower() in ("m_geldig_tot", "geldig_tot"):
            nieuwe_data.append(datetime(9000, 12, 31))
        elif col.lower() in ("m_is_actief", "is_actief"):
            nieuwe_data.append(True)
        elif col.lower() in("m_is_verwijderd", "is_verwijderd"):
            nieuwe_data.append(False)
        elif isinstance(col_type, DoubleType):
            nieuwe_data.append(0.00)
        elif isinstance(col_type, IntegerType) or isinstance(col_type, LongType):
            nieuwe_data.append(0)
        elif isinstance(col_type, StringType):
            nieuwe_data.append(onbekend_ongeldig)
        elif isinstance(col_type, BooleanType):
            nieuwe_data.append(False)
        elif isinstance(col_type, TimestampType):
            datum_tijd = datetime.strptime("9000-12-31 23:59:59", "%Y-%m-%d %H:%M:%S")
            nieuwe_data.append(datum_tijd)
        elif isinstance(col_type, DateType):
            datum_tijd = datetime.strptime("9000-12-31", "%Y-%m-%d").date()
            nieuwe_data.append(datum_tijd)
        elif isinstance(col_type, NullType):
            nieuwe_data.append(onbekend_ongeldig)
        elif isinstance(col_type, DecimalType):
            nieuwe_data.append(Decimal("0.00"))

    nieuwe_rij = Row(*nieuwe_data)
    schema = StructType([StructField(col, df.schema[col].dataType, True) for col in df.columns])

    # Bepaal het schema op basis van het dataframe
    rij = spark.createDataFrame([nieuwe_rij], schema)  # Maak een nieuwe rij

    return rij


def vul_lege_cellen_in(df: DataFrame, uitzonderings_kolommen: list = []):
    """
    Vult lege cellen in een DataFrame in met standaardwaarden, behalve voor de opgegeven uitzonderingskolommen.

    Args:
        df (DataFrame): Het DataFrame dat moet worden bewerkt.
        uitzonderings_kolommen (list, optional): Lijst van kolommen waarvoor lege cellen niet moeten worden ingevuld. Standaard is leeg.

    Returns:
        DataFrame: Het DataFrame met lege cellen ingevuld met standaardwaarden, behalve voor de uitzonderingskolommen.
    """
    for col_name in df.drop(*uitzonderings_kolommen).columns:
        col_type = df.schema[col_name].dataType
        if isinstance(col_type, DoubleType):
            df = (df.withColumn(col_name, when(column(col_name).isNull(), lit(0.00)).otherwise(column(col_name))))
        elif isinstance(col_type, IntegerType) or isinstance(col_type, LongType):
            df = (df.withColumn(col_name, when(column(col_name).isNull(), lit(0)).otherwise(column(col_name))))
        elif isinstance(col_type, StringType):
            df = (df.withColumn(col_name, when(column(col_name).isNull(), lit('N/A')).otherwise(column(col_name))))
        elif isinstance(col_type, BooleanType):
            df = (df.withColumn(col_name, when(column(col_name).isNull(), lit(None)).otherwise(column(col_name))))
        elif isinstance(col_type, TimestampType):
            datum_tijd = datetime.strptime("9999-12-31 23:59:59", "%Y-%m-%d %H:%M:%S")
            df = (df.withColumn(col_name, when(column(col_name).isNull(), lit(datum_tijd)).otherwise(column(col_name))))
        elif isinstance(col_type, DateType):
            datum_tijd = datetime.strptime("9999-12-31", "%Y-%m-%d").date()
            df = (df.withColumn(col_name, when(column(col_name).isNull(), lit(datum_tijd)).otherwise(column(col_name))))
    return df


def prepareer_dimensie_tabel(df: DataFrame, naam_bk: str, naam_id: str, uitzonderings_kolommen=[]):
    """
    Bereidt een dimensietabel voor door verschillende transformaties toe te passen.
    Deze functie voegt een unieke sleutel toe aan de tabel, voegt een "onbekende" dimensie toe, en vult lege cellen in de dimensietabel in.

    Args:
        df (DataFrame): Het DataFrame van de dimensietabel.
        naam_bk (str): Naam van de business key (kolom) in de tabel.
        naam_id (str): Naam van de ID-kolom in de tabel.
        uitzonderings_kolommen (list, optional): Lijst van kolommen waarvan de lege cellen niet ingevuld moeten worden. Standaard is leeg.

    Returns:
        DataFrame: Het voorbereide dimensietabel DataFrame na het toepassen van transformaties.
    """
    
    # Voeg unieke sleutel toe aan de tabel
    df_hashed = voeg_willekeurig_toe_en_hash_toe(df=df, business_key=naam_bk, naam_id=naam_id)

    # Voeg een "onbekende" dimensie toe aan de tabel
    df_hashed_onbekend = maak_onbekende_dimensie(df=df_hashed, naam_bk=naam_bk, naam_id=naam_id, uitzonderings_kolommen=[])

    # Vul de lege cellen in de dimensietabel in
    df_hashed_onbekend_ingevuld_lege_cellen = vul_lege_cellen_in(df=df_hashed_onbekend, uitzonderings_kolommen=[])

    return df_hashed_onbekend_ingevuld_lege_cellen

def prep_dataframe_for_dimensions(df: DataFrame, m_metadata: List, runtime: datetime, m_bron: str = None ) -> DataFrame:
    """
    Verrijkt een DataFrame met onbekende en ongeldige rijen, voegt metadata toe en slaat het resultaat op als een Spark SQL-tabel.

    Deze functie:
    - Voegt een rij toe voor 'onbekende' waarden en een rij voor 'ongeldige' waarden.
    - Genereert metadata (zoals verwerkingstijd en bron) via een hulpfunctie.
    - Slaat het verrijkte DataFrame op in de opgegeven doeltabel in de Spark catalogus.

    Parameters:
    -----------
    df : Het input DataFrame dat verrijkt en opgeslagen moet worden.
    m_metadata : Een lijst met metadata kolommen die moeten worden toegevoegd aan de DataFrame.
    runtime : De verwerkingstijd die wordt toegevoegd als metadata.
    m_bron : De broncode of -omschrijving die wordt toegevoegd als metadata.

    """
    naam_bk = next((col for col in df.columns if col.startswith("bk_")), None)
    naam_sid = next((col for col in df.columns if col.startswith("sid_")), None)

    #Union onbekend en ongeldige records
    onbekende_rij = maak_onbekende_ongeldige_dimensie(df=df,naam_bk=naam_bk, naam_sid=naam_sid)
    ongeldige_rij = maak_onbekende_ongeldige_dimensie(df=df,naam_bk=naam_bk, naam_sid=naam_sid, onbekend_ongeldig="ongeldig")

    #Onderstaande volgorde in de union is belangrijk te behouden, zodat ongeldig en onbekend altijd -2 en -1 waardes krijgen in de sid.
    df = (ongeldige_rij
            .union(onbekende_rij)
            .union(df)
            )

    df = siu.add_metadata_columns_to_dataframe(df=df, m_columns=m_metadata, runtime=runtime, bron=m_bron)

    return df

def run_dimensions(df: DataFrame, 
                   target_table: str, 
                   runtime: datetime, 
                   m_metadata: List, 
                   m_bron: str = None) -> None:
    """
    Laadt een dataframe in een dimensietabel met foutafhandeling en logger.

    Parameters:
        df: Het dataframe dat moet worden geladen.
        target_table: De naam van de doel-dimensietabel.
        runtime: De runtime van de operatie.
        m_metadata: Metadata die nodig is voor de verwerking.
        m_bron: De broncode of -omschrijving die wordt toegevoegd als metadata.
    """

    if df is None or df.isEmpty():
        logger.error("Het dataframe is leeg of ongeldig.")
        raise ValueError(f"Het dataframe voor tabel {target_table} is leeg. Pipeline wordt gestopt.")

    if not target_table:    
        raise ValueError("De target_table is niet opgegeven.")

    try:
        logger.info(f"Start prep dataframe voor dimensie {target_table}.")

        df = prep_dataframe_for_dimensions(df=df,
                                    runtime=runtime,
                                    m_metadata=m_metadata,
                                    m_bron=m_bron)
        
        logger.info(f"Eind prep dataframe voor dimensie {target_table}.")

        siu.write_to_table(df=df, target_table=target_table)

    except Exception as e:
        logger.error(f"Onverwachte fout bij het laden van data voor tabel {target_table}: {e}")