import pandas as pd
from odhpy import utils
from pandas.api.types import is_datetime64_any_dtype as is_datetime



def assert_df_format_standards(df: pd.DataFrame):
    """
    Args:
        df (pd.DataFrame): _description_
    """
    violations = check_df_format_standards(df)
    if len(violations) > 0:
        raise Exception(f"Dataframe does not meet odhpy format standards.\n {violations[0]}")
    


def check_df_format_standards(df: pd.DataFrame):
    """
    Checks if a given dataframe meets standards generally requried by 
    odhpy functions. These standards include:
    - Dataframe is not none
    - Dataframe index datatype is datetime
    - Dataframe index name is "Date"
    - Dataframe index values are daily sequential with no time-component
    - Data columns all have datatype of double
    - Missing values are nan (not na, not -nan)
    - Dataframe index name is

    Args:
        df (_type_): _description_

    Returns:
        bool: _description_
    """
    # - Dataframe is not none
    if df is None:
        return ["Dataframe is None"]
    # - Dataframe index datatype is datetime
    if not is_datetime(df.index):
        return ["Dataframe index type is not datetime"]
    # - Dataframe index name is "Date"
    if df.index.name != "Date":
        return ["Dataframe index name is not 'Date'"]
    # - Dataframe index values are daily sequential with no time-component
    start_date = min(df.index)
    expected_dates = utils.get_dates(start_date=start_date, days=len(df))
    for i in range(len(df)):
        if df.index.values[i] != expected_dates[i]:
            return [f"Unexpected datetime {df.index.values[i]} at index {i}"]
    # - Data columns all have datatype of double
    for c in df.columns:
        data_type = df[c].dtypes
        if (data_type != 'int64') and (df[c].dtypes != 'float64'):
            return [f"Column '{c}' is not int64 or float64: {data_type}"]
    # - Missing values are nan (not na, not -nan)
    # - Dataframe index name is
    return []



def set_index_dt(df: pd.DataFrame, dt_values=None, start_dt=None, **kwargs):
    """
    Returns a dataframe with datetimes as the index.     
    If no optional arguments are provided, the function will look for a column named "date" (not 
    case-sensitive) within the input dataframe. Otherwise dt_values or start_dt (assumes daily)
    may be provided. 

    Args:
        df (pd.DataFrame): _description_
        dt_values (_type_, optional): _description_. Defaults to None.
        start_dt (_type_, optional): _description_. Defaults to None.
    """
    if (df is None) or (len(df.columns) == 0):
        raise Exception("The dataframe is None or.")         
    answer = None
    n_days = len(df)

    # If start_dt or dt_values was provided, then we go ahead and use those. This could 
    # potentially override an existing "Date" column, but I assume thats what the user 
    # wants if used in that way.
    if start_dt != None:
        df["Date"] = utils.get_dates(start_dt, days=n_days)
    elif dt_values != None:
        if len(dt_values) < n_days:
            raise Exception("dt_values is shorter than the dataframe.") 
        df["Date"] = dt_values[:n_days]

    # Now if the index is datetimes (whether it already was, or was created above) we just 
    # need to make sure it is named "Date" and return the df.
    if is_datetime(df.index):
        df.index.name = "Date"
        return df

    # If there's no column named exactly "Date" (case sensitive) then we look for something 
    # ignoring whitespaces and capitalization and rename it exactly to "Date".
    if not "Date" in df.columns:
        for c in df.columns:
            if c.lower().strip() == "date":
                df = df.rename(columns={c: "Date"})
    
    # If there's still no "Date" column let's use the first column instead.
    if not "Date" in df.columns:
        df = df.rename(columns={df.columns[0]: "Date"})
    
    # Now, after all that, if there's a column called exactly "Date", we try to convert 
    # values to datetimes. 
    df["Date"] = pd.to_datetime(df["Date"], **kwargs)
    answer = df.set_index("Date")
    return answer



def check_data_equivalence(df1, df2, check_col_order=True, threshold=1e-6, details:dict=None):
    """Checks if two numeric dataframes are the same. It checks the names & order of the columns, 
    the values of the index, and summary stats on all the data columns.

    Args:
        df1 (_type_): First dataframe.
        df2 (_type_): Second dataframe.
        check_col_order (bool, optional): Specifies whether column order is important or not. Defaults to True.
        threshold (float, optional): Numerical threshold for checking if stats are the same. Defaults to 1e-6.
        details (dict, optional): This is a dicitonary for returning detailed results to the user. Defaults to None.

    Returns:
        bool: _description_
    """
    if details is None:
        details = {}
    # Check if columns are the same
    c1 = df1.columns
    c2 = df2.columns
    differences = set(c1).difference(set(df2))
    if len(differences) > 0:
        temp = differences.intersection(set(c1))
        details[len(details)] = f"{len(temp)} columns are unique to df1: {temp}"
        temp = differences.intersection(set(c2))
        details[len(details)] = f"{len(temp)} columns are unique to df2: {temp}"
        return False
    # Check column order
    if check_col_order:
        for i in range(len(c1)):
            if c1[i] != c2[i]:
                details[len(details)] = f"Column order difference at col {i}: '{c1[i]}' != '{c2[i]}'"
                return False
    # Check row count
    if len(df1) != len(df2):
        details[len(details)] = f"Row count difference: '{len(df1)}' != '{len(df2)}'"
        return False
    # Check values of indices
    for i in range(len(df1)):
        if df1.index[i] != df2.index[i]:
            details[len(details)] = f"Indices not matching at row {i}: {df1.index[i]} != {df2.index[i]}"
            return False
    # Check the stats
    stats1 = df1.describe()
    stats2 = df2.describe()
    for c in c1:
        for stat_name in stats1.index:
            val1 = stats1.loc[stat_name][c]
            val2 = stats2.loc[stat_name][c]
            if abs(val1 - val2) > threshold:
                details[len(details)] = f"Column '{c}' has a different {stat_name}: {val1} != {val2}"
                return False            
    # All checks passed
    return True
