import os
import shutil
import subprocess
import signal
import gradio as gr
from huggingface_hub import create_repo, HfApi, snapshot_download, whoami, ModelCard
from gradio_huggingfacehub_search import HuggingfaceHubSearch
from textwrap import dedent

# Obtener el token de Hugging Face desde el entorno
HF_TOKEN = os.getenv("HF_TOKEN", "")

def ensure_valid_token(oauth_token):
    """Verifica si el token es válido."""
    if not oauth_token or not oauth_token.strip():
        raise ValueError("Debe proporcionar un token válido.")
    return oauth_token.strip()

def generate_importance_matrix(model_path, train_data_path):
    """Genera la matriz de importancia usando llama-imatrix."""
    imatrix_command = f"./llama-imatrix -m ../{model_path} -f {train_data_path} -ngl 99 --output-frequency 10"
    
    os.chdir("llama.cpp")

    if not os.path.isfile(f"../{model_path}"):
        raise FileNotFoundError(f"Archivo del modelo no encontrado: {model_path}")

    process = subprocess.Popen(imatrix_command, shell=True)
    try:
        process.wait(timeout=60)
    except subprocess.TimeoutExpired:
        process.send_signal(signal.SIGINT)
        try:
            process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            process.kill()

    os.chdir("..")

def split_upload_model(model_path, repo_id, oauth_token, split_max_tensors=256, split_max_size=None):
    """Divide y sube el modelo en partes."""
    if not oauth_token or not oauth_token.strip():
        raise ValueError("Debe proporcionar un token válido.")
    
    split_cmd = f"llama.cpp/llama-gguf-split --split --split-max-tensors {split_max_tensors}"
    if split_max_size:
        split_cmd += f" --split-max-size {split_max_size}"
    split_cmd += f" {model_path} {model_path.split('.')[0]}"
    
    result = subprocess.run(split_cmd, shell=True, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"Error al dividir el modelo: {result.stderr}")

    sharded_model_files = [f for f in os.listdir('.') if f.startswith(model_path.split('.')[0])]
    if sharded_model_files:
        api = HfApi(token=oauth_token)
        for file in sharded_model_files:
            file_path = os.path.join('.', file)
            try:
                api.upload_file(
                    path_or_fileobj=file_path,
                    path_in_repo=file,
                    repo_id=repo_id,
                )
            except Exception as e:
                raise RuntimeError(f"Error al subir el archivo {file_path}: {e}")
    else:
        raise FileNotFoundError("No se encontraron archivos divididos.")
    
def process_model(model_id, q_method, use_imatrix, imatrix_q_method, private_repo, train_data_file, split_model, split_max_tensors, split_max_size, oauth_token):
    """Procesa el modelo descargado y realiza la conversión y subida."""
    token = ensure_valid_token(oauth_token)
    
    model_name = model_id.split('/')[-1]
    fp16 = f"{model_name}.fp16.gguf"

    try:
        api = HfApi(token=token)
        dl_pattern = [
            "*.safetensors", "*.bin", "*.pt", "*.onnx", "*.h5", "*.tflite",
            "*.ckpt", "*.pb", "*.tar", "*.xml", "*.caffemodel",
            "*.md", "*.json", "*.model"
        ]

        pattern = (
            "*.safetensors"
            if any(
                file.path.endswith(".safetensors")
                for file in api.list_repo_tree(
                    repo_id=model_id,
                    recursive=True,
                )
            )
            else "*.bin"
        )
        dl_pattern += pattern

        snapshot_download(repo_id=model_id, local_dir=model_name, local_dir_use_symlinks=False, allow_patterns=dl_pattern)
        print("Modelo descargado exitosamente!")

        conversion_script = "convert_hf_to_gguf.py"
        fp16_conversion = f"python llama.cpp/{conversion_script} {model_name} --outtype f16 --outfile {fp16}"
        result = subprocess.run(fp16_conversion, shell=True, capture_output=True)
        if result.returncode != 0:
            raise RuntimeError(f"Error al convertir a fp16: {result.stderr}")

        imatrix_path = "llama.cpp/imatrix.dat"
        if use_imatrix:
            if train_data_file:
                train_data_path = train_data_file.name
            else:
                train_data_path = "groups_merged.txt"

            if not os.path.isfile(train_data_path):
                raise FileNotFoundError(f"Archivo de datos de entrenamiento no encontrado: {train_data_path}")

            generate_importance_matrix(fp16, train_data_path)

        quantized_gguf_name = f"{model_name.lower()}-{imatrix_q_method.lower()}-imat.gguf" if use_imatrix else f"{model_name.lower()}-{q_method.lower()}.gguf"
        quantized_gguf_path = quantized_gguf_name
        
        quantise_ggml = f"./llama.cpp/llama-quantize {'--imatrix ' + imatrix_path if use_imatrix else ''} {fp16} {quantized_gguf_path} {imatrix_q_method if use_imatrix else q_method}"
        
        result = subprocess.run(quantise_ggml, shell=True, capture_output=True)
        if result.returncode != 0:
            raise RuntimeError(f"Error al cuantificar: {result.stderr}")

        username = whoami(token)["name"]
        new_repo_url = api.create_repo(repo_id=f"{username}/{model_name}-{imatrix_q_method if use_imatrix else q_method}-GGUF", exist_ok=True, private=private_repo)
        new_repo_id = new_repo_url.repo_id

        try:
            card = ModelCard.load(model_id, token=token)
        except:
            card = ModelCard("")
        if card.data.tags is None:
            card.data.tags = []
        card.data.tags.append("llama-cpp")
        card.data.tags.append("gguf-my-repo")
        card.data.base_model = model_id
        card.text = dedent(
            f"""
            # {new_repo_id}
            Este modelo fue convertido al formato GGUF desde [`{model_id}`](https://huggingface.co/{model_id}) usando llama.cpp a través del espacio GGUF-my-repo.
            Consulta el [card del modelo original](https://huggingface.co/{model_id}) para más detalles.

            ## Uso con llama.cpp
            Instala llama.cpp a través de brew (funciona en Mac y Linux)
            
            ```bash
            brew install llama.cpp
            ```

            Invoca el servidor llama.cpp o la CLI.

            ### CLI:
            ```bash
            llama-cli --hf-repo {new_repo_id} --hf-file {quantized_gguf_name} -p "El sentido de la vida y el universo es"
            ```

            ### Servidor:
            ```bash
            llama-server --hf-repo {new_repo_id} --hf-file {quantized_gguf_name} -c 2048
            ```

            Nota: También puedes usar este punto de control directamente a través de los [pasos de uso](https://github.com/ggerganov/llama.cpp?tab=readme-ov-file#usage) listados en el repositorio llama.cpp.
            """
        )
        card.save(f"README.md")

        if split_model:
            split_upload_model(quantized_gguf_path, new_repo_id, token, split_max_tensors, split_max_size)
        else:
            try:
                api.upload_file(
                    path_or_fileobj=quantized_gguf_path,
                    path_in_repo=quantized_gguf_name,
                    repo_id=new_repo_id,
                )
            except Exception as e:
                raise RuntimeError(f"Error al subir el modelo cuantificado: {e}")

        if os.path.isfile(imatrix_path):
            try:
                api.upload_file(
                    path_or_fileobj=imatrix_path,
                    path_in_repo="imatrix.dat",
                    repo_id=new_repo_id,
                )
            except Exception as e:
                raise RuntimeError(f"Error al subir imatrix.dat: {e}")

        api.upload_file(
            path_or_fileobj=f"README.md",
            path_in_repo=f"README.md",
            repo_id=new_repo_id,
        )

        return (
            f'Encuentra tu repositorio <a href=\'{new_repo_url}\' target="_blank" style="text-decoration:underline">aquí</a>',
            "llama.png",
        )
    except Exception as e:
        return (f"Error: {e}", "error.png")
    finally:
        shutil.rmtree(model_name, ignore_errors=True)

with gr.Blocks() as app:
    gr.Markdown("# Procesamiento de Modelos")
    
    gr.Markdown(
        """
        Este panel permite procesar modelos de machine learning, convertirlos al formato GGUF, y cargarlos en un repositorio de Hugging Face.
        Puedes seleccionar diferentes métodos de cuantización y personalizar la conversión usando matrices de importancia.
        """
    )
    
    with gr.Row():
        model_id = gr.Textbox(
            label="ID del Modelo",
            placeholder="username/model-name",
            info="Introduce el ID del modelo en Hugging Face que deseas procesar.",
        )
        q_method = gr.Dropdown(
            ["IQ3_M", "IQ3_XXS", "Q4_K_M", "Q4_K_S", "IQ4_NL", "IQ4_XS", "Q5_K_M", "Q5_K_S"],
            label="Método de Cuantización",
            info="Selecciona el método de cuantización que deseas aplicar al modelo."
        )
        use_imatrix = gr.Checkbox(
            label="Usar Matriz de Importancia",
            info="Marca esta opción si deseas usar una matriz de importancia para la cuantización."
        )
        imatrix_q_method = gr.Dropdown(
            ["IQ3_M", "IQ3_XXS", "Q4_K_M", "Q4_K_S", "IQ4_NL", "IQ4_XS", "Q5_K_M", "Q5_K_S"],
            label="Método de Matriz de Importancia",
            info="Selecciona el método de cuantización para la matriz de importancia.",
            visible=False  # Solo visible si se marca 'use_imatrix'
        )
        private_repo = gr.Checkbox(
            label="Repositorio Privado",
            info="Marca esta opción si deseas que el repositorio creado sea privado."
        )
        train_data_file = gr.File(
            label="Archivo de Datos de Entrenamiento",
            type="filepath",
            info="Selecciona el archivo que contiene los datos de entrenamiento necesarios para generar la matriz de importancia.",
        )
        split_model = gr.Checkbox(
            label="Dividir Modelo",
            info="Marca esta opción para dividir el modelo en partes más pequeñas antes de subirlo."
        )
        split_max_tensors = gr.Number(
            label="Max Tensors (para división)",
            value=256,
            info="Especifica el número máximo de tensores por parte si estás dividiendo el modelo.",
        )
        split_max_size = gr.Number(
            label="Max Tamaño (para división)",
            value=None,
            info="Especifica el tamaño máximo de cada parte si estás dividiendo el modelo.",
        )
        oauth_token = gr.Textbox(
            label="Token de Hugging Face",
            type="password",
            info="Introduce tu token de autenticación de Hugging Face. Asegúrate de que el token sea válido para acceder a los repositorios."
        )

    with gr.Row():
        result = gr.HTML(label="Resultado")
        img = gr.Image(label="Imagen")

    process_button = gr.Button("Procesar Modelo")
    process_button.click(
        process_model,
        inputs=[model_id, q_method, use_imatrix, imatrix_q_method, private_repo, train_data_file, split_model, split_max_tensors, split_max_size, oauth_token],
        outputs=[result, img]
    )

app.launch()