"""
Messa in Produzione di un Sistema per il Riconoscimento della Lingua di Testi per un Museo

Obiettivo del Progetto
Implementare un'API REST utilizzando Flask o FastAPI per esporre le funzionalità del modello di riconoscimento della lingua. Questa API dovrà:

-   Ricevere testi in formato JSON.
-   Restituire il codice della lingua riconosciuta.
-   Essere scalabile e pronta per l'integrazione con sistemi esterni.

Versioni librerie utilizzate da inserire nel file requirements.txt:
    fastapi>=0.110.0
    uvicorn>=0.28.0
    pydantic>=2.6.0
    numpy>=1.26.0
    scikit-learn>=1.5.0

"""


#Librerie
import uvicorn
import pickle
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, field_validator
import numpy as np
import logging


#Logging
logging.basicConfig(format='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level= logging.INFO, filename="logs.log", filemode="a") # Configurazione del logging per scrivere su un file di log
logger = logging.getLogger(__name__)


#Costanti
PORT = 8000 # Porta su cui viene esposto il server FastAPI
THRESHOLD_CONFIDENCE = 0.40 # Soglia di confidenza per la predizione
FILENAME = 'language_detection_pipeline.pkl' # Nome del file del modello salvato

# Funzione lettura caricamento file
def load_model():
    """
        Questa funzione legge il modello in un file pickle in lettura binaria e lo carica in memoria.
        Se il caricamento del modello va a buon fine, viene restituito.
        Se il file non esiste o è corrotto, viene sollevata un'eccezione HTTP con un messaggio di errore appropriato e status code 500.
    """
    try:
        with open(FILENAME, 'rb') as file:
            model = pickle.load(file)
            logger.info(f"Modello caricato correttamente da {FILENAME}")
            return model
    except pickle.UnpicklingError as e:
        logger.error(f"Il file del modello è corrotto o non valido: {e}")
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Il file del modello è corrotto o non valido")
    except FileNotFoundError:
        logger.error(f"File del modello non trovato: {FILENAME}")
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="File del modello non trovato")
    except Exception as e:
        logger.error(f"Errore imprevisto: {e}")
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Errore imprevisto durante il caricamento del modello")

# Classi per la richiesta e la risposta
class LanguageRequest(BaseModel):
    """
        Questa classe rappresenta la struttura della richiesta per l'identificazione della lingua.
        Contiene un campo "text" che rappresenta il testo da predire.
        Il campo deve essere una stringa, è obbligatorio e deve avere una lunghezza minima di 1 carattere.
    """
    model_config = {"extra": "forbid"} # Impedisce di inserire campi extra nella richiesta
    text: str = Field(..., min_length=1, description="Testo da predire") # Campo da predire

    @field_validator("text")
    @classmethod
    def validate_text(cls, value):
        """
            Questo metodo di validazione controlla che il campo text non sia vuoto o formato solo da spazi vuoti.
            Se il campo è vuoto o contiene solo spazi, viene sollevata un'eccezione HTTP con un messaggio di errore appropriato e status code 400.
        """
        if value.strip() == "":
            logger.error(f"Il campo text non può essere vuoto")
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Il campo text non può essere vuoto")
        return value

class LanguageResponse(BaseModel):
    """
        Questa classe rappresenta la struttura della risposta per l'identificazione della lingua.
        Contiene un campo "language_code" che rappresenta il codice della lingua rilevata e un campo "confidence" che rappresenta il livello di confidenza della predizione.
        Il campo "language_code" deve essere una stringa ed è obbligatorio.
        Il campo "confidence" deve essere un float ed è obbligatorio.
    """
    language_code: str = Field(..., description="Codice della lingua rilevata", examples= ["IT"]) # Codice della lingua rilevata
    confidence: float = Field(..., description="Livello di confidenza della predizione", examples= [0.69]) # Livello di confidenza della predizione


#Funzione controllo lingua
def validate_confidence(score):
    """
        Questa funzione controlla se il livello di confidenza della predizione è inferiore alla soglia definita.
        Se il livello di confidenza è inferiore alla soglia, viene sollevata un'eccezione HTTP con un messaggio di errore appropriato e status code 422.

        NOTA sul valore del THRESHOLD_CONFIDENCE:
        Il valore è stato fissato a 0.40 per trovare un compromesso tra accuratezza e affidabilità della predizione.
        Valori più alti avrebbero portato a un numero maggiore di predizioni affidabili, ma avrebbero anche escluso molte predizioni valide,
        mentre valori più bassi avrebbero aumentato il numero di predizioni, ma con un rischio maggiore di predizioni errate.
        La soglia è quindi stata scelta per bilanciare questi due aspetti, garantendo un buon livello di confidenza senza escludere troppe predizioni valide.
    """
    logger.debug(f"Score: {score}")
    if score <  THRESHOLD_CONFIDENCE: # Controllo se il livello di confidenza è inferiore alla soglia definita
        logger.error("La lingua è stata rilevata, ma la confidenza è troppo bassa per restituire un risultato affidabile")
        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="La lingua è stata rilevata, ma la confidenza è troppo bassa per restituire un risultato affidabile")


# Creazione dell'app FastAPI
app = FastAPI()
model = load_model()


# Endpoint per la predizione
@app.post('/identify-language', response_model = LanguageResponse)
def identify_language(request : LanguageRequest) -> LanguageResponse:
    logger.info("Richiesta identificazione testo")
    logger.debug(f"Testo da indentificare: {request.text}")
    probabilities = model.predict_proba([request.text])[0] # Calcola la probabilità per ogni lingua
    logger.info("Predizione completata")
    best_index = np.argmax(probabilities) # Trova la probabilità più alta
    predicted_language = str(model.classes_[best_index]) # Restituisce il codice della lingua predetta
    score = float(probabilities[best_index]) #Restituisce il livello di confidenza della predizione
    logger.debug(f"Confidenza calcolata: {round(score,4)}")
    validate_confidence(score) # Controlla il livello di confidenza

    logger.debug(f"Lingua predetta: {predicted_language}, confidence: {round(score,4)}")
    logger.info(f"Codice lingua: {predicted_language}, confidence: {round(score,4)}")
    logger.debug("Risposta inviata al client")
    return LanguageResponse(language_code= predicted_language, confidence=round(score,4)) # Restituisce la risposta al cliente con codice della lingua e livello di confidenza



# Run del server FastAPI
if __name__ == "__main__":
    logger.info(f"Avvio del server sulla porta: {PORT}")
    uvicorn.run("Prova_finale_modulo_Sviluppo_di_REST_API_ML.py:app", host="0.0.0.0", port=PORT)


"""
    Note finali:
    - Il codice è stato strutturato secondo la tecnica del clean coding utilizzando funzioni classi per dividere la responsabilità rendendo il codice leggibile e manutenibile.
    - Nel codice sono stati inseriti log per tracciare il flusso e dedurre il problema in caso di errore. I log vengono salvati in un file di log chiamato "logs.log" per motivi di audit e monitoraggio.
    - Il codice è stato commentato per dare la possibilità a chiunque di capire le funzionalità del codice e la logica di implementazione.
    - Tramite l'utilizzo di Pydantic,  è stato possibile fare dei controlli stringenti sui dati in input e in output, in modo da evitare problemi di tipo di dato
    - Il servizio di identificazione della lingua è in ascolto sulla porta 8000 e può essere testato tramite richieste HTTP POST all'endpoint /identify-language inserendo in input un testo in formato JSON.
    - Si è preferito utilizzare Uvicorn come server ASGI per la sua velocità e capacità di gestire richieste asincrone, rendendo l'API più performante e scalabile.


    Implementazione futura:
    - Aggiungere test unitari per verificare la corretta funzionalità di ogni singolo componente del sistema
    - Aggiungere test di integrazione per verificare il funzionamento dell'intero sistema
    - Deployare il servizio su un server cloud per renderlo accessibile
    - Aggiungere un sistema di autorizzazione tramite base auth o token per proteggere l'endpoint e limitare l'accesso solo agli utenti autorizzati
    - Integrare nginx per gestire connessioni HTTPS e bilanciamento del carico, migliorando la sicurezza e la scalabilità del servizio
"""