"""
function_test_OOS.py — Funzioni per la valutazione Out-of-Sample

Questo modulo contiene le funzioni di simulazione e visualizzazione usate per
valutare le performance di un assortimento ottimale su scenari out-of-sample,
cioè scenari non visti durante l'ottimizzazione.

Funzioni:

  run_out_of_sample()        : simula n_scenarios settimane di vendita con la soluzione data
                               e calcola vendite, rimanenze e lost service per ogni scenario.

  simulation_results_file()  : converte i risultati grezzi della simulazione in due DataFrame:
                                 - df_kpi    : KPI aggregati per scenario (vendite tot, lost service, ...)
                                 - df_details: dettaglio per scenario x prodotto

  plot_frequences()          : istogramma delle vendite totali e del lost service sui 300 scenari OOS.

  plot_heap_map()            : heatmap della domanda persa per prodotto x scenario, utile per
                               identificare prodotti critici e scenari peggiori.

Come funziona la simulazione in run_out_of_sample():

  Per ogni scenario (settimana):
    1. Genera la domanda giornaliera con make_arrivals() (arrivi Poisson + multinomial), metodo della classe ClientArrivals in models.py.
    2. Chiama make_sales_substitutions() per simulare le vendite giorno per giorno, metodo della classe ClientArrivals in models.py:
         - Vende quanto può dalla domanda primaria (limitato dallo stock).
         - Distribuisce la domanda insoddisfatta sulla matrice di sostituzione.
         - Tiene traccia di vendite, rimanenze, domanda persa e lost service.

Dove viene usata:

    Nei file main_test_OOS.py e main_test_shock.py, vengono utilizzate tutte le funzioni di function_test_OOS.py per ottenere i risultati e 
    la loro visualizzazione negli scenari simulati.  


"""

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

def run_out_of_sample(client_arrivals_obj, probabilities, z_assortment, initial_stock, subs_matrix, n_scenarios=300, seed=999):
    """
    esegue un test out-of-sample du n_scenarios scenari valutando l'assortimento ottimale.
    
    Input: 
        1. l'istanza della classe ClientArrivals, 
        2. le probabilità di scelta, 
        3. l'assortimento da valutare,
        4. con le quantità, 
        5. la matrice di sostituzione,
        6. il numero di scenari (default=300)
        7. il seed per la ripetitività (default=999)

    Output: 
        vendite, vendite_perse (contando no-buy), rimanenze, vendite perse senza no-buy e domanda iniziale per ogni scenario
    """
    
    np.random.seed(seed)
    #Generate scenarios
    scenarios_oos, _= client_arrivals_obj.make_arrivals(probabilities=probabilities,num_scenarios=n_scenarios)
    
    risults_oos = {}

    for s in range(n_scenarios):

        single_scenario=scenarios_oos[s] 
        demand_init=np.sum(single_scenario, axis=0) #domanda per ogni prodotto
        
        #Calcola KPI
        sales, lost, rest, lost_service = client_arrivals_obj.make_sales_substitutions(demand_scenario=single_scenario,z_assortment=z_assortment,initial_stock=initial_stock,subs_matrix=subs_matrix)
        
        risults_oos[f"OOS_Scenario_{s}"]=(sales, lost, rest, lost_service, demand_init)
        
    return risults_oos

def simulation_results_file(risults_scenarios, name_product):
    """
    funzione per generare i dataset e i file con le specifiche dei KPI per prodotto o per scenario x prodotto

    Input: output della funzione run_out_of_sample e il nome dei prodotti

    Output: 2 dataframe
     1. con le statistiche suddivise per prodotto
     2. con le statistiche suddivise per prodotto x scenario    
    
    """
    
    record_details = []
    record_kpi = []
    
    for scenario, (sales, lost, rest, lost_service, demand_init) in risults_scenarios.items():
        sales_ext=np.append(sales, 0)
        rest_ext=np.append(rest, 0)
        record_kpi.append({'Scenario':scenario,
            'Vendite Totali (Pezzi)':np.sum(sales),
            'Stock Rimanente Totale':np.sum(rest),
            'Lost Service (Domanda Insoddisfatta in store)':lost_service,
            'No-Buy (Fisiologico + Ripiego)':lost[-1]
        })
        
        for i, prod in enumerate(name_product):
            record_details.append({'Scenario':scenario,
                'Prodotto':prod,
                'Vendite':sales_ext[i],
                'Stock Rimanente':rest_ext[i],
                'Domanda Persa (Prodotto)':lost[i],
                'Domanda Iniziale':demand_init[i]
            })
            
    df_kpi=pd.DataFrame(record_kpi)
    df_details=pd.DataFrame(record_details)
    
    return df_kpi, df_details


def plot_frequences(df_kpi_oos):
    """funzione per ottenere i plot delle statistiche calcolate"""
    fig,axes=plt.subplots(1, 2, figsize=(15, 5))

    sns.histplot(data=df_kpi_oos, x='Vendite Totali (Pezzi)', kde=True, ax=axes[0], color='skyblue')
    axes[0].set_title('Distribuzione delle Vendite Totali\nnei 300 scenari Out-of-Sample')
    axes[0].set_xlabel('Pezzi Venduti')
    axes[0].set_ylabel('Frequenza (Numero di Settimane)')

    sns.histplot(data=df_kpi_oos, x='Lost Service (Domanda Insoddisfatta in store)', kde=True, ax=axes[1], color='salmon')
    axes[1].set_title('Distribuzione della Domanda Persa (Lost Service)\nnei 300 scenari Out-of-Sample')
    axes[1].set_xlabel('Pezzi Persi')
    axes[1].set_ylabel('Frequenza (Numero di Settimane)')

    plt.tight_layout()
    plt.show()

def plot_heap_map(df_filter, title, ylabel, xlabel, s_worst=None):
    """funzione per ottenere i plot delle statistiche calcolate"""
    if s_worst is None:
        scenarios_visual=[f'OOS_Scenario_{i}' for i in range(15)]
        pivot_lost=df_filter[df_filter['Scenario'].isin(scenarios_visual)].pivot(index='Prodotto',columns='Scenario',values='Domanda Persa (Prodotto)')
        plt.figure(figsize=(14, 10))
        sns.heatmap(pivot_lost, annot=True, fmt=".0f", cmap='YlOrRd', cbar_kws={'label': 'Pezzi Persi (Lost Service)'})
    else:
        pivot_lost=df_filter.pivot(index='Prodotto',columns='Scenario',values='Domanda Persa (Prodotto)')
        pivot_lost=pivot_lost[s_worst]
        plt.figure(figsize=(14, 10))
        sns.heatmap(pivot_lost, annot=True, fmt=".0f", cmap='Reds', cbar_kws={'label': 'Pezzi Persi (Lost Service)'})
    plt.title(title, pad=20, fontsize=14)
    plt.ylabel(ylabel, fontsize=12)
    plt.xlabel(xlabel, fontsize=12)
    plt.tight_layout() 

    plt.show()