"""
PIF Compiler - Workflow di elaborazione ordini

Contiene le classi Order e Project e le funzioni orchestratore
per il flusso di elaborazione degli ordini cosmetici.
"""

from datetime import datetime as dt
from typing import List, Optional

from pydantic import BaseModel, Field, ConfigDict, model_validator

from pif_compiler.classes.models import (
    StatoOrdine, Ingredient, Esposition
)
from pif_compiler.functions.db_utils import (
    db_connect, upsert_cliente, upsert_compilatore, aggiorna_stato_ordine,
    insert_ordine, get_oldest_pending_order,
    update_ordine_cliente, update_ordine_compilatore, update_ordine_progetto, update_ordine_note,
    get_preset_id_by_name, insert_progetto,
    insert_ingredient_lineage, get_ingrediente_id_by_cas,
    get_ordine_by_id, reset_ordine_per_retry
)
from pif_compiler.functions.common_log import get_logger

logger = get_logger()


# ==================== MODELS ====================

class ProjectIngredient(BaseModel):
    """Rappresenta un ingrediente nel contesto di un progetto."""
    cas: Optional[str] = None
    inci: Optional[str] = None
    percentage: float = 0.0
    is_colorante: bool = False
    skip_tox: bool = False
    ingredient: Optional[Ingredient] = None


class Order(BaseModel):
    """
    Ordine grezzo ricevuto dal front-end.
    Contiene i dati della tabella ordini + il JSON grezzo da MongoDB.
    """
    model_config = ConfigDict(arbitrary_types_allowed=True)

    # Attributi dalla tabella ordini
    id_ordine: int
    uuid_ordine: str
    id_cliente: Optional[int] = None
    id_compilatore: Optional[int] = None
    data_ordine: dt
    stato_ordine: StatoOrdine = StatoOrdine.RICEVUTO
    uuid_progetto: Optional[str] = None
    note: Optional[str] = None

    # JSON grezzo da MongoDB
    raw_json: dict = Field(default_factory=dict)

    # Campi derivati dal raw_json
    client_name: Optional[str] = None
    product_name: Optional[str] = None
    preset_name: Optional[str] = None
    ingredients_raw: list = Field(default_factory=list)

    @model_validator(mode='after')
    def parse_raw_json(self):
        """Parsa il raw_json per estrarre i campi principali."""
        if self.raw_json:
            self.client_name = self.raw_json.get('client_name')
            self.product_name = self.raw_json.get('product_name')
            self.preset_name = self.raw_json.get('preset_esposizione')
            self.ingredients_raw = self.raw_json.get('ingredients', [])
        return self

    @classmethod
    def pick_next(cls) -> Optional['Order']:
        """
        Recupera il prossimo ordine da elaborare.
        Prende il più vecchio con stato_ordine = RICEVUTO (1).
        """
        row = get_oldest_pending_order()
        if not row:
            logger.info("Nessun ordine pendente trovato")
            return None

        id_ordine, id_cliente, id_compilatore, uuid_ordine, uuid_progetto, data_ordine, stato_ordine, note = row

        # Recupera il JSON grezzo da MongoDB
        from bson import ObjectId
        collection = db_connect(collection_name='orders')
        doc = collection.find_one({"_id": ObjectId(uuid_ordine)})
        if not doc:
            logger.error(f"Documento MongoDB non trovato per uuid_ordine={uuid_ordine}")
            return None
        doc.pop("_id", None)

        logger.info(f"Ordine {id_ordine} recuperato (uuid={uuid_ordine})")

        return cls(
            id_ordine=id_ordine,
            uuid_ordine=uuid_ordine,
            id_cliente=id_cliente,
            id_compilatore=id_compilatore,
            data_ordine=data_ordine,
            stato_ordine=StatoOrdine(stato_ordine),
            uuid_progetto=uuid_progetto,
            note=note,
            raw_json=doc
        )

    def update_stato(self, nuovo_stato: StatoOrdine):
        """Aggiorna lo stato dell'ordine nel DB e nell'oggetto."""
        aggiorna_stato_ordine(self.id_ordine, nuovo_stato)
        self.stato_ordine = nuovo_stato
        logger.info(f"Ordine {self.id_ordine}: stato -> {nuovo_stato.name} ({nuovo_stato.value})")

    def validate_anagrafica(self):
        """
        Valida i dati anagrafici dell'ordine.
        - Verifica/crea il cliente nella tabella clienti
        - Aggiorna id_cliente sull'ordine
        - Stato -> VALIDATO
        """
        if not self.client_name:
            raise ValueError("Nome cliente mancante nel JSON dell'ordine")

        id_cliente = upsert_cliente(self.client_name)
        if id_cliente is None:
            raise ValueError(f"Errore nella validazione del cliente '{self.client_name}'")

        self.id_cliente = id_cliente
        update_ordine_cliente(self.id_ordine, id_cliente)
        logger.info(f"Ordine {self.id_ordine}: cliente '{self.client_name}' validato (id={id_cliente})")

        self.update_stato(StatoOrdine.VALIDATO)


class Project(BaseModel):
    """
    Progetto di valutazione cosmetica.
    Creato a partire da un Order, contiene gli ingredienti arricchiti
    e il preset di esposizione.
    """
    model_config = ConfigDict(arbitrary_types_allowed=True)

    order_id: int
    product_name: str
    client_name: str
    esposition: Esposition
    ingredients: List[ProjectIngredient] = Field(default_factory=list)

    @classmethod
    def from_order(cls, order: Order) -> 'Project':
        """Crea un Project a partire da un Order validato."""
        # Recupera il preset di esposizione
        preset_name = order.preset_name
        if not preset_name:
            raise ValueError("Nome preset esposizione mancante nell'ordine")

        esposition = Esposition.get_by_name(preset_name)
        if not esposition:
            raise ValueError(f"Preset esposizione '{preset_name}' non trovato nel database")

        logger.info(f"Ordine {order.id_ordine}: preset '{preset_name}' recuperato")

        # Parsa gli ingredienti dal JSON grezzo
        project_ingredients = []
        for ing_raw in order.ingredients_raw:
            cas_value = ing_raw.get('cas', '') or None
            if cas_value == '':
                cas_value = None

            pi = ProjectIngredient(
                cas=cas_value,
                inci=ing_raw.get('inci'),
                percentage=ing_raw.get('percentage', 0),
                is_colorante=ing_raw.get('is_colorante', False),
                skip_tox=ing_raw.get('skip_tox', False)
            )
            project_ingredients.append(pi)

        logger.info(f"Ordine {order.id_ordine}: {len(project_ingredients)} ingredienti parsati")

        return cls(
            order_id=order.id_ordine,
            product_name=order.product_name or "",
            client_name=order.client_name or "",
            esposition=esposition,
            ingredients=project_ingredients
        )

    def process_ingredients(self):
        """
        Arricchisce gli ingredienti tramite scraping (ECHA, PubChem, COSING).
        Salta gli ingredienti con skip_tox=True o CAS vuoto.
        """
        enriched = 0
        skipped = 0

        for pi in self.ingredients:
            if pi.skip_tox or not pi.cas:
                logger.info(f"Skip ingrediente: inci={pi.inci}, cas={pi.cas}, skip_tox={pi.skip_tox}")
                skipped += 1
                continue

            try:
                logger.info(f"Arricchimento ingrediente CAS={pi.cas}...")
                inci_list = [pi.inci] if pi.inci else None
                ingredient = Ingredient.get_or_create(pi.cas, inci=inci_list)
                pi.ingredient = ingredient
                enriched += 1
                logger.info(f"Ingrediente CAS={pi.cas} arricchito: {ingredient.get_stats()}")
            except Exception as e:
                logger.error(f"Errore arricchimento CAS={pi.cas}: {e}", exc_info=True)
                continue

        logger.info(f"Arricchimento completato: {enriched} arricchiti, {skipped} saltati")

    def save(self) -> Optional[str]:
        """
        Salva il progetto su MongoDB e PostgreSQL.
        1. Dump su MongoDB collection 'projects'
        2. Aggiorna ordini.uuid_progetto
        3. Inserisci nella tabella progetti
        4. Inserisci relazioni ingredients_lineage
        """
        # 1. Dump su MongoDB
        collection = db_connect(collection_name='projects')
        mongo_dict = self.model_dump(mode='json')
        result = collection.insert_one(mongo_dict)
        uuid_progetto = str(result.inserted_id)

        logger.info(f"Progetto salvato su MongoDB: uuid_progetto={uuid_progetto}")

        # 2. Aggiorna ordini.uuid_progetto
        update_ordine_progetto(self.order_id, uuid_progetto)

        # 3. Recupera id_preset e inserisci in progetti
        id_preset = get_preset_id_by_name(self.esposition.preset_name)
        id_progetto = insert_progetto(uuid_progetto, id_preset)

        if id_progetto:
            logger.info(f"Progetto inserito in PostgreSQL: id={id_progetto}")

            # 4. Inserisci ingredients_lineage per ogni ingrediente arricchito
            for pi in self.ingredients:
                if pi.ingredient and pi.cas:
                    id_ingrediente = get_ingrediente_id_by_cas(pi.cas)
                    if id_ingrediente:
                        insert_ingredient_lineage(id_progetto, id_ingrediente)
                        logger.info(f"Lineage: progetto={id_progetto}, ingrediente CAS={pi.cas}")
        else:
            logger.error("Errore inserimento progetto in PostgreSQL")

        return uuid_progetto

    def export_excel(self, output_path: str = None) -> str:
        """Esporta il progetto in un file Excel. Ritorna il percorso del file."""
        from pif_compiler.functions.excel_export import export_project_excel
        return export_project_excel(self, output_path)


# ==================== ORCHESTRATOR ====================

def receive_order(raw_json: dict, compiler_name: Optional[str] = None) -> Optional[int]:
    """
    Riceve un ordine dal front-end, lo salva su MongoDB e crea il record in PostgreSQL.
    Se compiler_name è fornito, crea/recupera il compilatore e lo lega subito all'ordine.
    Ritorna id_ordine.
    """
    # 1. Risolvi il compilatore prima di salvare, così è già nel record
    id_compilatore = None
    if compiler_name:
        id_compilatore = upsert_compilatore(compiler_name)
        if id_compilatore is None:
            logger.warning(f"receive_order: upsert compilatore '{compiler_name}' fallito, ordine creato senza compilatore")

    # 2. Salva il JSON grezzo su MongoDB collection 'orders'
    collection = db_connect(collection_name='orders')
    result = collection.insert_one(raw_json.copy())
    uuid_ordine = str(result.inserted_id)

    logger.info(f"Ordine salvato su MongoDB: uuid_ordine={uuid_ordine}")

    # 3. Crea il record nella tabella ordini (stato = RICEVUTO) con compilatore già valorizzato
    id_ordine = insert_ordine(uuid_ordine, id_compilatore=id_compilatore)
    if id_ordine is None:
        logger.error(f"Errore creazione record ordini per uuid={uuid_ordine}")
        return None

    logger.info(f"Ordine {id_ordine} creato in PostgreSQL (stato=RICEVUTO, compilatore={'id=' + str(id_compilatore) if id_compilatore else 'n/d'})")
    return id_ordine


def process_order_pipeline():
    """
    Pipeline di elaborazione ordine. Eseguita come background task.
    1. Recupera il prossimo ordine pendente (più vecchio con stato RICEVUTO)
    2. Valida anagrafica -> stato VALIDATO
    3. Crea il Project -> stato COMPILAZIONE
    4. Arricchisce gli ingredienti
    5. Stato ARRICCHITO
    6. Salva il progetto su MongoDB + PostgreSQL
    """
    order = None

    try:
        # 1. Recupera il prossimo ordine
        order = Order.pick_next()
        if order is None:
            logger.warning("Pipeline: nessun ordine pendente da elaborare")
            return

        logger.info(f"Pipeline: inizio elaborazione ordine {order.id_ordine}")

        # 2. Valida anagrafica -> VALIDATO
        order.validate_anagrafica()

        # 3. Crea il Project -> COMPILAZIONE
        order.update_stato(StatoOrdine.COMPILAZIONE)
        project = Project.from_order(order)
        logger.info(f"Pipeline: progetto creato con {len(project.ingredients)} ingredienti")

        # 4. Arricchisci gli ingredienti
        project.process_ingredients()

        # 5. Stato ARRICCHITO
        order.update_stato(StatoOrdine.COMPLETATO)

        # 6. Salva il progetto
        uuid_progetto = project.save()
        order.uuid_progetto = uuid_progetto

        logger.info(f"Pipeline: ordine {order.id_ordine} completato (uuid_progetto={uuid_progetto})")

    except Exception as e:
        logger.error(f"Pipeline: errore elaborazione ordine: {e}", exc_info=True)
        if order:
            order.update_stato(StatoOrdine.ERRORE)
            update_ordine_note(order.id_ordine, f"Errore pipeline: {str(e)}")


def retry_order(id_ordine: int) -> bool:
    """
    Resetta un ordine in stato ERRORE a RICEVUTO per rielaborarlo.
    Ritorna True se il reset è avvenuto, False altrimenti.
    """
    row = get_ordine_by_id(id_ordine)
    if not row:
        logger.warning(f"Retry: ordine {id_ordine} non trovato")
        return False

    stato_attuale = row[6]  # stato_ordine
    if stato_attuale != StatoOrdine.ERRORE:
        logger.warning(f"Retry: ordine {id_ordine} non è in stato ERRORE (stato={stato_attuale})")
        return False

    result = reset_ordine_per_retry(id_ordine)
    if result:
        logger.info(f"Retry: ordine {id_ordine} resettato a RICEVUTO")
        return True

    logger.error(f"Retry: errore nel reset dell'ordine {id_ordine}")
    return False


def trigger_pipeline():
    """
    Lancia manualmente una esecuzione della pipeline.
    Processa il prossimo ordine pendente (più vecchio con stato RICEVUTO).
    """
    logger.info("Pipeline manuale: avvio")
    process_order_pipeline()
    logger.info("Pipeline manuale: completata")
