import pandas as pd
import numpy as np
import datetime as dt
import csv
import os
from ecopipeline.unit_convert import energy_to_power, energy_btu_to_kwh, energy_kwh_to_kbtu, power_flow_to_kW
from ecopipeline.config import _input_directory, _output_directory

pd.set_option('display.max_columns', None)


def concat_last_row(df: pd.DataFrame, last_row: pd.DataFrame) -> pd.DataFrame:
    """
    Function takes in a dataframe and the last row from the SQL database and concatenates the last row
    to the start of the dataframe

    Args: 
        df (pd.DataFrame): Pandas dataframe  
        last_row (pd.DataFrame): last row Pandas dataframe
    Returns: 
        pd.DataFrame: Pandas dataframe with last row concatenated
    """
    df = pd.concat([last_row, df], join="inner")
    return df


def round_time(df: pd.DataFrame):
    """
    Function takes in a dataframe and rounds dataTime index to the nearest minute. Works in place

    Args: 
        df (pd.DataFrame): Pandas dataframe
    Returns: 
        None
    """
    if (df.empty):
        return False
    df.index = df.index.round('T')
    return True


def rename_sensors(df: pd.DataFrame, variable_names_path: str = f"{_input_directory}Variable_Names.csv", site: str = ""):
    """
    Function will take in a dataframe and a string representation of a file path and renames
    sensors from their alias to their true name.

    Args: 
        df (pd.DataFrame): Pandas dataframe
        variable_names_path (str): file location of file containing sensor aliases to their corresponding name (default value of Variable_Names.csv)
        site (str): strin of site name (default to empty string)
    Returns: 
        pd.DataFrame: Pandas dataframe
    """
    try:
        variable_data = pd.read_csv(variable_names_path)
    except FileNotFoundError:
        print("File Not Found: ", variable_names_path)
        return

    if (site != ""):
        variable_data = variable_data.loc[variable_data['site'] == site]
    
    variable_data = variable_data[['variable_alias', 'variable_name']]
    variable_data.dropna(axis=0, inplace=True)
    variable_alias = list(variable_data["variable_alias"])
    variable_true = list(variable_data["variable_name"])
    variable_alias_true_dict = dict(zip(variable_alias, variable_true))

    df.rename(columns=variable_alias_true_dict, inplace=True)

    # drop columns that do not have a corresponding true name
    df.drop(columns=[col for col in df if col in variable_alias], inplace=True)

    # drop columns that are not documented in variable names csv file at all
    df.drop(columns=[col for col in df if col not in variable_true], inplace=True)


def avg_duplicate_times(df: pd.DataFrame, timezone : str) -> pd.DataFrame:
    """
    Function will take in a dataframe and look for duplicate timestamps due to 
    daylight savings. Takes the average values between the duplicate timestamps.

    Args: 
        df (pd.DataFrame): Pandas dataframe
        timezone (str): Timezone as a string
    Returns: 
        pd.DataFrame: Pandas dataframe 
    """
    df.index = pd.DatetimeIndex(df.index).tz_localize(None)
    df = df.groupby(df.index).mean()
    df.index = (df.index).tz_localize(timezone)
    return df

def _rm_cols(col, bounds_df):  # Helper function for remove_outliers
    """
    Function will take in a pandas series and bounds information
    stored in a dataframe, then check each element of that column and set it to nan
    if it is outside the given bounds. 

    Args: 
        col (pd.Series): Pandas series
        bounds_df (pd.DataFrame): Pandas dataframe
    Returns: 
        None 
    """
    if (col.name in bounds_df.index):
        c_lower = float(bounds_df.loc[col.name]["lower_bound"])
        c_upper = float(bounds_df.loc[col.name]["upper_bound"])
        # for this to be one line, it could be the following:
        #col.mask((col > float(bounds_df.loc[col.name]["upper_bound"])) | (col < float(bounds_df.loc[col.name]["lower_bound"])), other = np.NaN, inplace = True)
        col.mask((col > c_upper) | (col < c_lower), other=np.NaN, inplace=True)

# TODO: remove_outliers STRETCH GOAL: Functionality for alarms being raised based on bounds needs to happen here.


def remove_outliers(df: pd.DataFrame, variable_names_path: str = f"{_input_directory}Variable_Names.csv", site: str = "") -> pd.DataFrame:
    """
    Function will take a pandas dataframe and location of bounds information in a csv,
    store the bounds data in a dataframe, then remove outliers above or below bounds as 
    designated by the csv. Function then returns the resulting dataframe. 

    Args: 
        df (pd.DataFrame): Pandas dataframe
        variable_names_path (str): file location of file containing sensor aliases to their corresponding name (default value of Variable_Names.csv)
        site (str): strin of site name (default to empty string)
    Returns: 
        pd.DataFrame: Pandas dataframe
    """
    try:
        bounds_df = pd.read_csv(variable_names_path)
    except FileNotFoundError:
        print("File Not Found: ", variable_names_path)
        return df

    if (site != ""):
        bounds_df = bounds_df.loc[bounds_df['site'] == site]

    bounds_df = bounds_df.loc[:, [
        "variable_name", "lower_bound", "upper_bound"]]
    bounds_df.dropna(axis=0, thresh=2, inplace=True)
    bounds_df.set_index(['variable_name'], inplace=True)
    bounds_df = bounds_df[bounds_df.index.notnull()]

    df.apply(_rm_cols, args=(bounds_df,))
    return df


def _ffill(col, ffill_df, previous_fill: pd.DataFrame = None):  # Helper function for ffill_missing
    """
    Function will take in a pandas series and ffill information from a pandas dataframe,
    then for each entry in the series, either forward fill unconditionally or up to the 
    provided limit based on the information in provided dataframe. 

    Args: 
        col (pd.Series): Pandas series
        ffill_df (pd.DataFrame): Pandas dataframe
    Returns: 
        None (df is modified, not returned)
    """
    if (col.name in ffill_df.index):
        #set initial fill value where needed for first row
        if previous_fill is not None and len(col) > 0 and pd.isna(col.iloc[0]):
            col.iloc[0] = previous_fill[col.name].iloc[0]
        cp = ffill_df.loc[col.name]["changepoint"]
        length = ffill_df.loc[col.name]["ffill_length"]
        if (length != length):  # check for nan, set to 0
            length = 0
        length = int(length)  # casting to int to avoid float errors
        if (cp == 1):  # ffill unconditionally
            col.fillna(method='ffill', inplace=True)
        elif (cp == 0):  # ffill only up to length
            col.fillna(method='ffill', inplace=True, limit=length)


def ffill_missing(df: pd.DataFrame, vars_filename: str = f"{_input_directory}Variable_Names.csv", previous_fill: pd.DataFrame = None) -> pd.DataFrame:
    """
    Function will take a pandas dataframe and forward fill select variables with no entry. 
    Args: 
        df (pd.DataFrame): Pandas dataframe
        variable_names_path (str): file location of file containing sensor aliases to their corresponding name (default value of Variable_Names.csv)
    Returns: 
        pd.DataFrame: Pandas dataframe
    """
    try:
        # ffill dataframe holds ffill length and changepoint bool
        ffill_df = pd.read_csv(vars_filename)
    except FileNotFoundError:
        print("File Not Found: ", vars_filename)
        return df

    ffill_df = ffill_df.loc[:, [
        "variable_name", "changepoint", "ffill_length"]]
    # drop data without changepoint AND ffill_length
    ffill_df.dropna(axis=0, thresh=2, inplace=True)
    ffill_df.set_index(['variable_name'], inplace=True)
    ffill_df = ffill_df[ffill_df.index.notnull()]  # drop data without names

    df.apply(_ffill, args=(ffill_df,previous_fill))
    return df

def sensor_adjustment(df: pd.DataFrame) -> pd.DataFrame:
    """
    Reads in input/adjustments.csv and applies necessary adjustments to the dataframe

    Args: 
        df (pd.DataFrame): DataFrame to be adjusted
    Returns: 
        pd.DataFrame: Adjusted Dataframe
    """
    try:
        adjustments = pd.read_csv(f"{_input_directory}adjustments.csv")
    except FileNotFoundError:
        print("File Not Found: ", f"{_input_directory}adjustments.csv")
        return df
    if adjustments.empty:
        return df

    adjustments["datetime_applied"] = pd.to_datetime(
        adjustments["datetime_applied"])
    df = df.sort_values(by="datetime_applied")

    for adjustment in adjustments:
        adjustment_datetime = adjustment["datetime_applied"]
        # NOTE: To access time, df.index (this returns a list of DateTime objects in a full df)
        # To access time object if you have located a series, it's series.name (ex: df.iloc[0].name -- this prints the DateTime for the first row in a df)
        df_pre = df.loc[df.index < adjustment_datetime]
        df_post = df.loc[df.index >= adjustment_datetime]
        match adjustment["adjustment_type"]:
            case "add":
                continue
            case "remove":
                df_post[adjustment["sensor_1"]] = np.nan
            case "swap":
                df_post[[adjustment["sensor_1"], adjustment["sensor_2"]]] = df_post[[
                    adjustment["sensor_2"], adjustment["sensor_1"]]]
        df = pd.concat([df_pre, df_post], ignore_index=True)

    return df


# def get_energy_by_min(df: pd.DataFrame) -> pd.DataFrame:
#     """
#     Energy is recorded cummulatively. Function takes the lagged differences in 
#     order to get a per/minute value for each of the energy variables.

#     Args: 
#         df (pd.DataFrame): Pandas dataframe
#     Returns: 
#         pd.DataFrame: Pandas dataframe
#     """
#     energy_vars = df.filter(regex=".*Energy.*")
#     energy_vars = energy_vars.filter(regex=".*[^BTU]$")
#     for var in energy_vars:
#         df[var] = df[var] - df[var].shift(1)
#     return df


# def verify_power_energy(df: pd.DataFrame):
#     """
#     Verifies that for each timestamp, corresponding power and energy variables are consistent
#     with one another. Power ~= energy * 60. Margin of error TBD. Outputs to a csv file any
#     rows with conflicting power and energy variables.

#     Prereq: 
#         Input dataframe MUST have had get_energy_by_min() called on it previously
#     Args: 
#         df (pd.DataFrame): Pandas dataframe
#     Returns:
#         None
#     """

#     out_df = pd.DataFrame(columns=['time_pt', 'power_variable', 'energy_variable',
#                           'energy_value', 'power_value', 'expected_power', 'difference_from_expected'])
#     energy_vars = (df.filter(regex=".*Energy.*")).filter(regex=".*[^BTU]$")
#     power_vars = (df.filter(regex=".*Power.*")
#                   ).filter(regex="^((?!Energy).)*$")
#     df['time_pt'] = df.index
#     power_energy_df = df[df.columns.intersection(
#         ['time_pt'] + list(energy_vars) + list(power_vars))]
#     del df['time_pt']

#     margin_error = 5.0          # margin of error still TBD, 5.0 for testing purposes
#     for pvar in power_vars:
#         if (pvar != 'PowerMeter_SkidAux_Power'):
#             corres_energy = pvar.replace('Power', 'Energy')
#         if (pvar == 'PowerMeter_SkidAux_Power'):
#             corres_energy = 'PowerMeter_SkidAux_Energty'
#         if (corres_energy in energy_vars):
#             temp_df = power_energy_df[power_energy_df.columns.intersection(['time_pt'] + list(energy_vars) + list(power_vars))]
#             for i, row in temp_df.iterrows():
#                 expected = energy_to_power(row[corres_energy])
#                 low_bound = expected - margin_error
#                 high_bound = expected + margin_error
#                 if (row[pvar] != expected):
#                     out_df.loc[len(df.index)] = [row['time_pt'], pvar, corres_energy,
#                                                  row[corres_energy], row[pvar], expected, abs(expected - row[pvar])]
#                     path_to_output = f'{_output_directory}power_energy_conflicts.csv'
#                     if not os.path.isfile(path_to_output):
#                         out_df.to_csv(path_to_output, index=False, header=out_df.columns)
#                     else:
#                         out_df.to_csv(path_to_output, index=False, mode='a', header=False)


# def aggregate_values(df: pd.DataFrame, thermo_slice: str) -> pd.DataFrame:
#     """
#     Gets daily average of data for all relevant varibles. 

#     Args: 
#         df (pd.DataFrame): Pandas DataFrame of minute by minute data
#         thermo_slice (str): indicates the time at which slicing begins. If none no slicing is performed. The format of the thermo_slice string is "HH:MM AM/PM".

#     TODO: FIX RETURN VALUE
#     Returns: 
#         pd.DataFrame: Pandas DataFrame which contains the aggregated hourly data.
#     """
#     avg_sd = df[['Temp_RecircSupply_MXV1', 'Temp_RecircSupply_MXV2', 'Flow_CityWater_atSkid', 'Temp_PrimaryStorageOutTop',
#                  'Temp_CityWater_atSkid', 'Flow_SecLoop', 'Temp_SecLoopHexOutlet', 'Temp_SecLoopHexInlet', 'Flow_CityWater', 'Temp_CityWater',
#                  'Flow_RecircReturn_MXV1', 'Temp_RecircReturn_MXV1', 'Flow_RecircReturn_MXV2', 'Temp_RecircReturn_MXV2', 'PowerIn_SecLoopPump',
#                  'EnergyIn_HPWH']].resample('D').mean()

#     if thermo_slice is not None:
#         avg_sd_6 = df.between_time(thermo_slice, "11:59PM")[
#             ['Temp_CityWater_atSkid', 'Temp_CityWater']].resample('D').mean()
#     else:
#         avg_sd_6 = df[['Temp_CityWater_atSkid',
#                        'Temp_CityWater']].resample('D').mean()

#     cop_inter = pd.DataFrame(index=avg_sd.index)
#     cop_inter['Temp_RecircSupply_avg'] = (
#         avg_sd['Temp_RecircSupply_MXV1'] + avg_sd['Temp_RecircSupply_MXV2']) / 2
#     cop_inter['HeatOut_PrimaryPlant'] = energy_kwh_to_kbtu(avg_sd['Flow_CityWater_atSkid'],
#                                                            avg_sd['Temp_PrimaryStorageOutTop'] -
#                                                            avg_sd['Temp_CityWater_atSkid'])
#     cop_inter['HeatOut_PrimaryPlant_dyavg'] = energy_kwh_to_kbtu(avg_sd['Flow_CityWater_atSkid'],
#                                                                  avg_sd['Temp_PrimaryStorageOutTop'] -
#                                                                  avg_sd_6['Temp_CityWater_atSkid'])
#     cop_inter['HeatOut_SecLoop'] = energy_kwh_to_kbtu(avg_sd['Flow_SecLoop'], avg_sd['Temp_SecLoopHexOutlet'] -
#                                                       avg_sd['Temp_SecLoopHexInlet'])
#     cop_inter['HeatOut_HW'] = energy_kwh_to_kbtu(avg_sd['Flow_CityWater'], cop_inter['Temp_RecircSupply_avg'] -
#                                                  avg_sd['Temp_CityWater'])
#     cop_inter['HeatOut_HW_dyavg'] = energy_kwh_to_kbtu(avg_sd['Flow_CityWater'], cop_inter['Temp_RecircSupply_avg'] -
#                                                        avg_sd_6['Temp_CityWater'])
#     cop_inter['HeatLoss_TempMaint_MXV1'] = energy_kwh_to_kbtu(avg_sd['Flow_RecircReturn_MXV1'],
#                                                               avg_sd['Temp_RecircSupply_MXV1'] -
#                                                               avg_sd['Temp_RecircReturn_MXV1'])
#     cop_inter['HeatLoss_TempMaint_MXV2'] = energy_kwh_to_kbtu(avg_sd['Flow_RecircReturn_MXV2'],
#                                                               avg_sd['Temp_RecircSupply_MXV2'] -
#                                                               avg_sd['Temp_RecircReturn_MXV2'])
#     cop_inter['EnergyIn_SecLoopPump'] = avg_sd['PowerIn_SecLoopPump'] * \
#         (1/60) * (1/1000)
#     cop_inter['EnergyIn_HPWH'] = avg_sd['EnergyIn_HPWH']

#     return cop_inter


# def calculate_cop_values(df: pd.DataFrame, heatLoss_fixed: int, thermo_slice: str) -> pd.DataFrame:
#     """
#     Performs COP calculations using the daily aggregated data. 

#     Args: 
#         df (pd.DataFrame): Pandas DataFrame to add COP columns to
#         heatloss_fixed (float): fixed heatloss value 
#         thermo_slice (str): the time at which slicing begins if we would like to thermo slice. 

#     Returns: 
#         pd.DataFrame: Pandas DataFrame with the added COP columns. 
#     """
#     cop_inter = pd.DataFrame()
#     if (len(df) != 0):
#         cop_inter = aggregate_values(df, thermo_slice)

#     cop_values = pd.DataFrame(index=cop_inter.index, columns=[
#                               "COP_DHWSys", "COP_DHWSys_dyavg", "COP_DHWSys_fixTMloss", "COP_PrimaryPlant", "COP_PrimaryPlant_dyavg"])

#     try:
#         cop_values['COP_DHWSys'] = (energy_btu_to_kwh(cop_inter['HeatOut_HW']) + (
#             energy_btu_to_kwh(cop_inter['HeatLoss_TempMaint_MXV1'])) + (
#             energy_btu_to_kwh(cop_inter['HeatLoss_TempMaint_MXV2']))) / (
#                 cop_inter['EnergyIn_HPWH'] + cop_inter['EnergyIn_SecLoopPump'])

#         if thermo_slice is not None:
#             cop_values['COP_DHWSys_dyavg'] = (energy_btu_to_kwh(cop_inter['HeatOut_HW_dyavg']) + (
#                 energy_btu_to_kwh(cop_inter['HeatLoss_TempMaint_MXV1'])) + (
#                 energy_btu_to_kwh(cop_inter['HeatLoss_TempMaint_MXV2']))) / (
#                     cop_inter['EnergyIn_HPWH'] + cop_inter['EnergyIn_SecLoopPump'])

#         cop_values['COP_DHWSys_fixTMloss'] = ((energy_btu_to_kwh(cop_inter['HeatOut_HW'])) + (
#             energy_btu_to_kwh(heatLoss_fixed))) / ((cop_inter['EnergyIn_HPWH'] +
#                                                     cop_inter['EnergyIn_SecLoopPump']))

#         cop_values['COP_PrimaryPlant'] = (energy_btu_to_kwh(cop_inter['HeatOut_PrimaryPlant'])) / \
#             (cop_inter['EnergyIn_HPWH'] + cop_inter['EnergyIn_SecLoopPump'])

#         if thermo_slice is not None:
#             cop_values['COP_PrimaryPlant_dyavg'] = (energy_btu_to_kwh(cop_inter['HeatOut_PrimaryPlant_dyavg'])) / \
#                 (cop_inter['EnergyIn_HPWH'] +
#                  cop_inter['EnergyIn_SecLoopPump'])

#     except ZeroDivisionError:
#         print("DIVIDED BY ZERO ERROR")
#         return df

#     return cop_values

def cop_method_2(df: pd.DataFrame, cop_tm, cop_primary_column_name):
    # TODO this is specific for Maria and Antonia
    """
    Performs COP calculation method 2 as deffined by Scott's whiteboard image
    COP = COP_primary(ELEC_primary/ELEC_total) + COP_tm(ELEC_tm/ELEC_total)

    Args: 
        df (pd.DataFrame): Pandas DataFrame to add COP columns to
        cop_tm (float): fixed COP value for temputure Maintenece system
        cop_primary_column_name (str): Name of the column used for COP_Primary values

    Returns: 
        pd.DataFrame: Pandas DataFrame with the added COP columns. 
    """
    columns_to_check = [cop_primary_column_name, 'PowerIn_Total']

    missing_columns = [col for col in columns_to_check if col not in df.columns]

    if missing_columns:
        print('Cannot calculate COP as the following columns are missing from the DataFrame:', missing_columns)
        return df
    
    # Create list of column names to sum
    sum_primary_cols = [col for col in df.columns if col.startswith('PowerIn_HPWH') or col == 'PowerIn_SecLoopPump']
    sum_tm_cols = [col for col in df.columns if col.startswith('PowerIn_SwingTank')]

    if len(sum_primary_cols) == 0:
        print('Cannot calculate COP as the primary power columns (such as PowerIn_HPWH and PowerIn_SecLoopPump) are missing from the DataFrame')
        return df

    if len(sum_tm_cols) == 0:
        print('Cannot calculate COP as the temperature maintenance power columns (such as PowerIn_SwingTank) are missing from the DataFrame')
        return df
    
    # Create new DataFrame with one column called 'PowerIn_Primary' that contains the sum of the specified columns
    sum_power_in_df = pd.DataFrame({'PowerIn_Primary': df[sum_primary_cols].sum(axis=1),
                                    'PowerIn_TM': df[sum_tm_cols].sum(axis=1)})

    df['COP_DHWSys_2'] = (df[cop_primary_column_name] * (sum_power_in_df['PowerIn_Primary']/df['PowerIn_Total'])) + (cop_tm * (sum_power_in_df['PowerIn_TM']/df['PowerIn_Total']))
    return df

# NOTE: Move to bayview.py
# loops through a list of dateTime objects, compares if the date of that object matches the
# date of the row name, which is also a dateTime object. If it matches, load_shift is True (happened that day)
def _ls_helper(row, dt_list):
    """
    Function takes in a pandas series and a list of dates, then checks
    each entry in the series and if it matches a date in the list of dates,
    sets the series load_shift_day to True. 
    Args: 
        row (pd.Series): Pandas series 
        list (<class 'list'>): Python list
    Output: 
        row (pd.Series): Pandas series
    """
    for date in dt_list:
        if (row.name.date() == date.date()):
            row.loc["load_shift_day"] = True
    return row

# NOTE: Move to bayview.py
def aggregate_df(df: pd.DataFrame):
    """
    Function takes in a pandas dataframe of minute data, aggregates it into hourly and daily 
    dataframes, appends some loadshift data onto the daily df, and then returns those. 
    Args: 
        df (pd.DataFrame): Single pandas dataframe of minute-by-minute sensor data.
    Returns: 
        pd.DataFrame: Two pandas dataframes, one of by the hour and one of by the day aggregated sensor data.
    """
    # If df passed in empty, we just return empty dfs for hourly_df and daily_df
    if (df.empty):
        return pd.DataFrame(), pd.DataFrame()

    # Start by splitting the dataframe into sum, which has all energy related vars, and mean, which has everything else. Time is calc'd differently because it's the index
    sum_df = (df.filter(regex=".*Energy.*")).filter(regex=".*[^BTU]$")
    # NEEDS TO INCLUDE: EnergyOut_PrimaryPlant_BTU
    mean_df = df.filter(regex="^((?!Energy)(?!EnergyOut_PrimaryPlant_BTU).)*$")

    # Resample downsamples the columns of the df into 1 hour bins and sums/means the values of the timestamps falling within that bin
    hourly_sum = sum_df.resample('H').sum()
    hourly_mean = mean_df.resample('H').mean()
    # Same thing as for hours, but for a whole day
    daily_sum = sum_df.resample("D").sum()
    daily_mean = mean_df.resample('D').mean()

    # combine sum_df and mean_df into one hourly_df, then try and print that and see if it breaks
    hourly_df = pd.concat([hourly_sum, hourly_mean], axis=1)
    daily_df = pd.concat([daily_sum, daily_mean], axis=1)

    # appending loadshift data
    filename = f"{_input_directory}loadshift_matrix.csv"
    date_list = []
    with open(filename) as datefile:
        readCSV = csv.reader(datefile, delimiter=',')
        for row in readCSV:
            date_list.append(row[0])
        date_list.pop(0)
    # date_list is a list of strings in the following format: "1/19/2023", OR "%m/%d/%Y", now we convert to datetime!
    format = "%m/%d/%Y"
    dt_list = []
    for date in date_list:
        dt_list.append(dt.datetime.strptime(date, format))
    daily_df["load_shift_day"] = False
    daily_df = daily_df.apply(_ls_helper, axis=1, args=(dt_list,))
    
    # if any day in hourly table is incomplete, we should delete that day from the daily table as the averaged data it contains will be from an incomplete day.
    complete_days = hourly_df.resample('D').apply(lambda x: len(x) == 24).values.flatten()
    return hourly_df, daily_df[complete_days]

# def set_zone_vol(location: pd.Series, gals: int, total: int, zones: pd.Series) -> pd.DataFrame:
#     """
#     Function that initializes the dataframe that holds the volumes of each zone.

#     Args:
#         location (pd.Series)
#         gals (int) 
#         total (int) 
#         zones (pd.Series)
#     Returns: 
#         pd.DataFrame: Pandas dataframe
#     """
#     relative_loc = location
#     tank_frxn = relative_loc.subtract(relative_loc.shift(-1))
#     gal_per_tank = gals
#     tot_storage = total
#     zone_gals = tank_frxn * tot_storage
#     zone_gals = pd.Series.dropna(zone_gals)  # remove NA from leading math
#     zone_list = zones
#     gals_per_zone = pd.DataFrame({'Zone': zone_list, 'Zone_vol_g': zone_gals})
#     return gals_per_zone


# def _largest_less_than(df_row: pd.Series, target: int) -> str:
#     """
#     Function takes a list of gz/json filenames and a target temperature and determines
#     the zone with the highest temperature < 120 degrees.

#     Args: 
#         df_row (pd.DataFrame): A single row of a sensor Pandas Dataframe in a series 
#         target (int): integer target
#     Output: 
#         str: A string of the name of the zone.
#     """
#     count = 0
#     largest_less_than_120_tmp = []
#     for val in df_row:
#         if val < target:
#             largest_less_than_120_tmp = df_row.index[count]
#             break
#         count = count + 1

#     return largest_less_than_120_tmp


# def _get_vol_equivalent_to_120(df_row: pd.Series, location: pd.Series, gals: int, total: int, zones: pd.Series) -> float:
#     """
#     Function takes a row of sensor data and finds the total volume of water > 120 degrees.

#     Args: 
#         df_row (pd.Series) 
#         location (pd.Series)
#         gals (int)
#         total (int)
#         zones (pd.Series)
#     Returns: 
#         float: A float of the total volume of water > 120 degrees
#     """
#     try:
#         tvadder = 0
#         vadder = 0
#         gals_per_zone = set_zone_vol(location, gals, total, zones)
#         dfcheck = df_row.filter(regex='top|mid|bottom')
#         # An empty or invalid dataframe would have Vol120 and ZoneTemp120 as columns with
#         # values of 0, so we check if the size is 0 without those columns if the dataframe has no data.
#         if (dfcheck.size == 0):
#             return 0
#         dftemp = df_row.filter(
#             regex='Temp_CityWater_atSkid|HPWHOutlet$|top|mid|bottom|120')
#         count = 1
#         for val in dftemp:
#             if dftemp.index[count] == "Temp_low":
#                 vadder += gals_per_zone[gals_per_zone.columns[1]][count]
#                 tvadder += val * gals_per_zone[gals_per_zone.columns[1]][count]
#                 break
#             elif dftemp[dftemp.index[count + 1]] >= 120:
#                 vadder += gals_per_zone[gals_per_zone.columns[1]][count]
#                 tvadder += (dftemp[dftemp.index[count + 1]] + val) / \
#                     2 * gals_per_zone[gals_per_zone.columns[1]][count]
#             elif dftemp[dftemp.index[count + 1]] < 120:
#                 vadder += dftemp.get('Vol120')
#                 tvadder += dftemp.get('Vol120') * dftemp.get('ZoneTemp120')
#                 break
#             count += 1
#         avg_temp_above_120 = tvadder / vadder
#         temp_ratio = (avg_temp_above_120 - dftemp[0]) / (120 - dftemp[0])
#         return (temp_ratio * vadder)
#     except ZeroDivisionError:
#         print("DIVIDED BY ZERO ERROR")
#         return 0


# def _get_V120(df_row: pd.Series, location: pd.Series, gals: int, total: int, zones: pd.Series):
#     """
#     Function takes a row of sensor data and determines the volume of water > 120 degrees
#     in the zone that has the highest sensor < 120 degrees.

#     Args: 
#         df_row (pd.Series): A single row of a sensor Pandas Dataframe in a series
#         location (pd.Series)
#         gals (int)
#         total (int)
#         zones (pd.Series)
#     Returns: 
#         float: A float of the total volume of water > 120 degrees     
#     """
#     try:
#         gals_per_zone = set_zone_vol(location, gals, total, zones)
#         temp_cols = df_row.filter(regex='HPWHOutlet$|top|mid|bottom')
#         if (temp_cols.size <= 3):
#             return 0
#         name_cols = ""
#         name_cols = _largest_less_than(temp_cols, 120)
#         count = 0
#         for index in temp_cols.index:
#             if index == name_cols:
#                 name_col_index = count
#                 break
#             count += 1
#         dV = gals_per_zone['Zone_vol_g'][name_col_index]
#         V120 = (temp_cols[temp_cols.index[name_col_index]] - 120) / (
#             temp_cols[temp_cols.index[name_col_index]] - temp_cols[temp_cols.index[name_col_index - 1]]) * dV
#         return V120
#     except ZeroDivisionError:
#         print("DIVIDED BY ZERO ERROR")
#         return 0


# def _get_zone_Temp120(df_row: pd.Series) -> float:
#     """
#     Function takes a row of sensor data and determines the highest sensor < 120 degrees.

#     Args: 
#         df_row (pd.Series): A single row of a sensor Pandas Dataframe in a series
#     Returns: 
#         float: A float of the average temperature of the zone < 120 degrees
#     """
#     # if df_row["Temp_120"] != 120:
#     #    return 0
#     temp_cols = df_row.filter(regex='HPWHOutlet$|top|mid|bottom')
#     if (temp_cols.size <= 3):
#         return 0
#     name_cols = _largest_less_than(temp_cols, 120)
#     count = 0
#     for index in temp_cols.index:
#         if index == name_cols:
#             name_col_index = count
#             break
#         count += 1

#     zone_Temp_120 = (120 + temp_cols[temp_cols.index[name_col_index - 1]]) / 2
#     return zone_Temp_120


# def get_storage_gals120(df: pd.DataFrame, location: pd.Series, gals: int, total: int, zones: pd.Series) -> pd.DataFrame:
#     """
#     Function that creates and appends the Gals120 data onto the Dataframe

#     Args: 
#         df (pd.Series): A Pandas Dataframe
#         location (pd.Series)
#         gals (int)
#         total (int)
#         zones (pd.Series)
#     Returns: 
#         pd.DataFrame: a Pandas Dataframe
#     """
#     if (len(df) > 0):
#         df['Vol120'] = df.apply(_get_V120, args=(
#             location, gals, total, zones), axis=1)
#         df['ZoneTemp120'] = df.apply(_get_zone_Temp120, axis=1)
#         df['Vol_Equivalent_to_120'] = df.apply(
#             _get_vol_equivalent_to_120, args=(location, gals, total, zones), axis=1)

#     return df


# def _calculate_average_zone_temp(df, substring):
#     try:
#         df_subset = df[[x for x in df if substring in x]]
#         result = df_subset.sum(axis=1, skipna=True) / df_subset.count(axis=1)
#         return result
#     except ZeroDivisionError:
#         print("DIVIDED BY ZERO ERROR")
#         return 0


# def get_temp_zones120(df) -> pd.DataFrame:
#     df['Temp_top'] = _calculate_average_zone_temp(df, "Temp1")
#     df['Temp_midtop'] = _calculate_average_zone_temp(df, "Temp2")
#     df['Temp_mid'] = _calculate_average_zone_temp(df, "Temp3")
#     df['Temp_midbottom'] = _calculate_average_zone_temp(df, "Temp4")
#     df['Temp_bottom'] = _calculate_average_zone_temp(df, "Temp5")
#     return df


def join_to_hourly(hourly_data: pd.DataFrame, noaa_data: pd.DataFrame) -> pd.DataFrame:
    """
    Function left-joins the weather data to the hourly dataframe.

    Args: 
        hourly_data (pd.DataFrame):Hourly dataframe
        noaa_data (pd.DataFrame): noaa dataframe
    Returns: 
        pd.DataFrame: A single, joined dataframe
    """
    out_df = hourly_data.join(noaa_data)
    return out_df


def join_to_daily(daily_data: pd.DataFrame, cop_data: pd.DataFrame) -> pd.DataFrame:
    """
    Function left-joins the the daily data and COP data.

    Args: 
        daily_data (pd.DataFrame): Daily dataframe
        cop_data (pd.DataFrame): cop_values dataframe
    Returns: 
        pd.DataFrame: A single, joined dataframe
    """
    out_df = daily_data.join(cop_data)
    return out_df
