import json as js
import re
import requests as req
from typing import Union
from pif_compiler.services.common_log import get_logger

logger = get_logger()

#region Funzione che processa una lista di CAS presa da Cosing

def parse_cas_numbers(cas_string:list) -> list:
    logger.debug(f"Parsing CAS numbers from input: {cas_string}")

    # Siccome ci assicuriamo esternamente che esista almeno un cas possiamo prendere la stringa
    cas_string = cas_string[0]
    logger.debug(f"Extracted CAS string: {cas_string}")

    # Rimuoviamo parentesi e il loro contenuto
    cas_string = re.sub(r"\([^)]*\)", "", cas_string)
    logger.debug(f"After removing parentheses: {cas_string}")

    # Eseguiamo uno split su vari possibili separatori
    cas_parts = re.split(r"[/;,]", cas_string)

    # Otteniamo una lista utilizzando i cas precedenti e rimuovendo spazi in eccesso
    cas_list = [cas.strip() for cas in cas_parts]
    logger.debug(f"CAS list after splitting: {cas_list}")

    # Alcuni cas sono divisi da un doppio dash (--) che dobbiamo rimuovere
    # è però necessario farlo ora in seconda battuta

    if len(cas_list) == 1 and "--" in cas_list[0]:
        logger.debug("Found double dash separator, splitting further")
        cas_list = [cas.strip() for cas in cas_list[0].split("--")]

    # Siccome alcuni cas hanno valori non validi ("-") li cerchiamo e rimuoviamo
    if cas_list:

       while "-" in cas_list:
           logger.debug("Removing invalid CAS value: '-'")
           cas_list.remove("-")

    logger.info(f"Parsed CAS numbers: {cas_list}")
    return cas_list
#endregion

#region Funzione per eseguire una ricerca direttamente sul cosing

# Il primo argomento è la stringa da cercare, il secondo indica il tipo di ricerca
def cosing_search(text : str, mode : str = "name") -> Union[list,dict,None]:
    logger.info(f"Starting COSING search: text='{text}', mode='{mode}'")

    cosing_post_req = "https://api.tech.ec.europa.eu/search-api/prod/rest/search?apiKey=285a77fd-1257-4271-8507-f0c6b2961203&text=*&pageSize=100&pageNumber=1"
    agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"

    # La modalità di ricerca base è quella per nome, che sia INCI o di altro tipo
    if mode == "name":
        logger.debug("Search mode: name (INCI, chemical name, etc.)")
        search_query = {"bool":
                         {"must":[
                             {"text":
                              {"query":f"{text}","fields":
                               ["inciName.exact","inciUsaName","innName.exact","phEurName","chemicalName","chemicalDescription"],
                               "defaultOperator":"AND"}}]}}

    # In caso di ricerca per numero cas o EC il payload della richiesta è diverso
    elif mode in ["cas","ec"]:
        logger.debug(f"Search mode: {mode}")
        search_query = {"bool": {"must": [{"text": {"query": f"*{text}*","fields": ["casNo", "ecNo"]}}]}}

    # La ricerca per ID è necessaria dove serva recuperare gli identified ingredients
    elif mode == "id":
        logger.debug("Search mode: substance ID")
        search_query = {"bool":{"must":[{"term":{"substanceId":f"{text}"}}]}}

    # Se la mode inserita non è prevista lancio un errore
    else:
        logger.error(f"Invalid search mode: {mode}")
        raise ValueError(f"Invalid search mode: {mode}")

    # Creo il payload della mia request
    files = {"query": ("query",js.dumps(search_query),"application/json")}
    logger.debug(f"Search query: {search_query}")

    # Eseguo la post di ricerca
    try:
        logger.debug(f"Sending POST request to COSING API")
        risposta = req.post(cosing_post_req,headers={"user_agent":agent,"Connection":"keep-alive"},files=files)
        risposta.raise_for_status()
        risposta = risposta.json()

        if risposta["results"]:
            logger.info(f"COSING search successful: found {len(risposta['results'])} result(s)")
            logger.debug(f"First result substance ID: {risposta['results'][0]['metadata'].get('substanceId', 'N/A')}")
            return risposta["results"][0]["metadata"]
        else:
            # La funzione ritorna None quando non ho risultati dalla mia ricerca
            logger.warning(f"COSING search returned no results for text='{text}', mode='{mode}'")
            return None

    except req.exceptions.RequestException as e:
        logger.error(f"HTTP request error during COSING search: {e}")
        raise
    except (KeyError, ValueError, TypeError) as e:
        logger.error(f"Error parsing COSING response: {e}")
        raise
#endregion

#region Funzione per pulire un json cosing e restituirlo

def clean_cosing(json : dict, full : bool = True) -> dict:
    substance_id = json.get("substanceId", ["Unknown"])[0] if json.get("substanceId") else "Unknown"
    logger.info(f"Cleaning COSING data for substance ID: {substance_id}, full={full}")

    # Definisco i campi di nostro interesse e li divido in base al tipo di output finale che desidero

    string_cols = [
        "itemType",
        "nameOfCommonIngredientsGlossary",
        "inciName",
        "phEurName",
        "chemicalName",
        "innName",
        "substanceId",
        "refNo"
        ]

    list_cols = [
        "casNo",
        "ecNo",
        "functionName",
        "otherRestrictions",
        "sccsOpinion",
        "sccsOpinionUrls",
        "identifiedIngredient",
        "annexNo",
        "otherRegulations"
        ]

    # Creo una lista con tutti i campi su cui ciclare

    total_keys = string_cols + list_cols

    # Definisco la base dell"url che mi serve per ottenere il link cosing della sostanza

    base_url = "https://ec.europa.eu/growth/tools-databases/cosing/details/"
    clean_json = {}

    # Ciclo su tutti i campi di interesse

    for key in total_keys:

        # Alcuni campi contengono una dicitura inutile che occupa solo spazio
        # per cui provvedo a rimuoverla

        while "<empty>" in json[key]:
            json[key].remove("<empty>")

        # Se il campo dovrà avere in output una lista, posso anche accettare le liste vuote del cosing come valore

        if key in list_cols:
            value = json[key]

            # Il cas e l"ec sono casi speciali, per cui eseguo delle funzioni in più quando lo tratto

            if key in ["casNo", "ecNo"]:
                if value:
                    logger.debug(f"Processing {key}: {value}")
                    value = parse_cas_numbers(value)

            # Completiamo direttamente il json di ritorno ove presenti degli identifiedIngredient,
            # solo dove il flag "full" è true

            elif key == "identifiedIngredient":
                if full:
                    if value:
                        logger.debug(f"Processing {len(value)} identified ingredient(s)")
                        value = identified_ingredients(value)

            clean_json[key] = value

        else:

            # Questo nome di campo era troppo lungo e ho preferito semplificarlo

            if key == "nameOfCommonIngredientsGlossary":
                nKey = "commonName"

            # Dovendo rinominare alcuni campi, negli altri casi copio il nome iniziale

            else:
                nKey = key

            # Siccome voglio in output una stringa semplice e se usassi uno slicer su di una lista vuota
            # devo prima verificare che la lista cosing contenga dei valori

            if json[key]:
                clean_json[nKey] = json[key][0]
            else:
                clean_json[nKey] = ""

    # Il campo cosingUrl non esiste ancora, vado a crearlo unendo il substance ID all"url base

    clean_json["cosingUrl"] = f"{base_url}{json["substanceId"][0]}"
    logger.debug(f"Generated COSING URL: {clean_json['cosingUrl']}")
    logger.info(f"Successfully cleaned COSING data for substance ID: {substance_id}")

    return clean_json
#endregion

#region Funzione per completare, se necessario, un json cosing

def identified_ingredients(id_list : list) -> list:
    logger.info(f"Processing {len(id_list)} identified ingredient(s): {id_list}")

    identified = []

    # Per ognuno degli ingredienti in IdentifiedIngredients eseguo una ricerca

    for id in id_list:
        logger.debug(f"Searching for identified ingredient with ID: {id}")

        ingredient = cosing_search(id,"id")

        if ingredient:

            # Vado a pulire i json appena trovati

            ingredient = clean_cosing(ingredient,full=False)

            # Ora salvo nella lista il documento pulito

            identified.append(ingredient)
            logger.debug(f"Successfully added identified ingredient ID: {id}")
        else:
            logger.warning(f"Could not find identified ingredient with ID: {id}")

    # Finito di popolare la lista con gli oggetti degli identifiedIngredient, la ritorno

    logger.info(f"Successfully processed {len(identified)} of {len(id_list)} identified ingredient(s)")
    return identified
#endregion

if __name__ == "__main__":
    print(cosing_search("Water","name"))
    
    