import funcy
import gc
import os
import json
import pathlib
import re
import tempfile
import time
import pandas as pd

import torch
import modal
from modal import enter
import numpy as np
import torchaudio
from itertools import combinations

from suno_utils.utils.display import suppress_logging
from contextlib import redirect_stderr
from suno_utils.utils.s3 import read_from_s3, upload_s3_files, list_s3_dir
from suno_utils.utils.text import read_jsonl, write_jsonl
from suno_utils.tasks.data_loader import load_audio_mp

from suno_utils.gpt.generation import GenerationConfig
from suno_utils.gpt.engine import Engine
from suno_utils.gpt.generation_engine import make_request
from suno_utils.diffusion.generation import preload_diff_models, generate

from suno_utils.worker.modal_base import MODAL_MOUNTS


MOUNT_PATH = "/suno/models"

aws_secret = modal.Secret.from_name("studio-aws")
SECRETS = [
    aws_secret,
    modal.Secret.from_dict(
        {
            "SUNO_ASSETS_PATH": "/suno/models/assets",
            "XDG_CACHE_HOME": "/suno/models/",
        }
    ),
    modal.Secret.from_name("api-callback-token"),
    modal.Secret.from_name("datadog-metrics"),
]

base_image = (
    modal.Image.debian_slim()
    .apt_install(
        "curl", "ffmpeg", "sox", "unzip", "libsox-fmt-mp3", "zlib1g-dev", "git"
    )
    .run_commands(
        [
            'curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"',
            "unzip -q awscliv2.zip",
            "./aws/install",
        ]
    )
    .dockerfile_commands(
        [
            "COPY --from=datadog/serverless-init:1.2.1 /datadog-init /app/datadog-init",
            'ENTRYPOINT ["/app/datadog-init"]',
        ]
    )
    .pip_install("torch==2.4.0")
    .pip_install_private_repos(
        "github.com/suno-ai/glockenspiel.git@f05e2f251#subdirectory=descript-audio-codec&egg=descript-audio-codec",
        git_user="mcamac",
        secrets=[modal.Secret.from_name("victor-modal-github-token")],
    )
    .pip_install(
        "boto3",
        "transformers",
        "tokenizers",
        "encodec",
        "ctc_segmentation",
        "psutil",
        "redis",
        "gradio",
        "pydantic",
        "nnAudio",
        "rpyc",
        "biopython>=1.81",  # TODO: don't love this depdendency, for hoot
        "pynvml",  # for torch cuda utilization
        "torchsde",
    )
    .pip_install_from_pyproject(
        str(pathlib.Path(__file__).parent.parent.parent.parent / "pyproject.toml"),
    )
    .pip_install("torch==2.4.0")  # this is cause flash-attn can't work with 2.5 yet
    .run_commands(
        "FLASH_ATTENTION_SKIP_CUDA_BUILD=TRUE pip install flash-attn==2.6.3 --no-build-isolation",
    )
    .pip_install(
        "torch==2.5.1",
        "torchaudio==2.5.1",
    )
)


def setup():

    N_BATCH = 2
    engine = Engine(
        "/app/suno/models/chirp_v2/tokenizer_60k.json",
        max_sequences=4 * N_BATCH,
        compile=False,
    )
    cfg = engine.model.config

    diff_model_fp = (
        "/app/suno/checkpoints/2024-12-10_19-53-58_s7792/step_3000_infer.pt"  # prod
    )
    # diff_model_fp = "/app/suno/checkpoints/2025-01-19_05-10-13_s1983/step_3000_infer.pt"  # best
    # diff_model_fp = "/app/suno/checkpoints/2024-12-16_15-54-26_s3031/last_ckpt_infer.pt"
    # diff_model_fp = "s3://suno-data/georg/tmp/2b_prefix_ft.pt"

    # codec_filepath = "s3://suno-data/minz/models/dac_vae_fixed_25hz.pth"
    codec_filepath = "s3://suno-data/christian/25hz_vae_peaq_kl_0.005.pth"

    if "dac_vae_fixed" in codec_filepath or "dac_vae_tuned" in codec_filepath:
        patch_size = 1
        codec_scale_factor = 0.4
    elif "convnext_vae_tuned" in codec_filepath:
        patch_size = 1
        codec_scale_factor = 1.0
    else:
        patch_size = 1
        codec_scale_factor = 2.5

    _ = preload_diff_models(
        tokenizer_filepath="/home/georg/notebooks/gpu_nb/tmp/tokenizer_60k.json",
        semantic_model_filepath="/home/georg/notebooks/gpu_nb/tmp/mert_25.pt",
        semantic_clusters_filepath="/home/georg/notebooks/gpu_nb/tmp/mert_25_2x4k.npy",
        codec_filepath=codec_filepath,
        dit_model_filepath=diff_model_fp,
        model_type="prefix",
        weights_precision=torch.bfloat16,
        compile=False,
        # codec_scale_factor=codec_scale_factor,
    )


class GenerateWorker:
    def __init__(self, gpt_ckpt: str, diff_ckpt: str, output_path: str):
        self.output_path = output_path
        start_time = time.time()
        print("Start loading models")

        # load gpt model
        num_gpus = torch.cuda.device_count()
        cuda_device = torch.cuda.current_device()
        print(f"Found {num_gpus} GPUs. Using GPU {cuda_device}.")

        gpt_ckpt_path = chirp_v2._get_model_if_needed(gpt_ckpt, cache_dir=MOUNT_PATH)
        tokenizer_path = chirp_v2._get_model_if_needed(
            chirp_v2.TOKENIZER_PATH, cache_dir=MOUNT_PATH
        )

        min_batch_size = min(32, max_sequences)
        if MODEL_CONFIG.min_batch_size is not None:
            min_batch_size = MODEL_CONFIG.min_batch_size

        tokenizer_path = diffusion_gen.get_model_if_needed(
            diffusion_gen.TOKENIZER_FILEPATH, cache_dir=MOUNT_PATH
        )
        semantic_model_path = diffusion_gen.get_model_if_needed(
            diffusion_gen.SEMANTIC_MODEL_FILEPATH, cache_dir=MOUNT_PATH
        )
        semantic_clusters_path = diffusion_gen.get_model_if_needed(
            diffusion_gen.SEMANTIC_CLUSTERS_FILEPATH, cache_dir=MOUNT_PATH
        )
        codec_path = diffusion_gen.get_model_if_needed(
            diffusion_gen.CODEC_FILEPATH, cache_dir=MOUNT_PATH
        )
        dit_model_path = diffusion_gen.get_model_if_needed(
            diffusion_gen.DIT_MODEL_FILEPATH, cache_dir=MOUNT_PATH
        )

        diffusion_gen.preload_models(
            tokenizer_filepath=tokenizer_path,
            semantic_model_filepath=semantic_model_path,
            semantic_clusters_filepath=semantic_clusters_path,
            codec_filepath=codec_path,
            dit_model_filepath=dit_model_path,
            compile=False,  # can't compile with flash v2)
        )
        print(
            f"Finish loading models. Took {round(time.time() - start_time, 2)} seconds"
        )

    @staticmethod
    def download_models(dir_path=MOUNT_PATH):
        """Download diffusion models."""
        print("Start downloading models")
        _ = diffusion_gen.get_model_if_needed(
            diffusion_gen.TOKENIZER_FILEPATH, cache_dir=dir_path
        )
        _ = diffusion_gen.get_model_if_needed(
            diffusion_gen.SEMANTIC_MODEL_FILEPATH, cache_dir=dir_path
        )
        _ = diffusion_gen.get_model_if_needed(
            diffusion_gen.SEMANTIC_CLUSTERS_FILEPATH, cache_dir=dir_path
        )
        _ = diffusion_gen.get_model_if_needed(
            diffusion_gen.CODEC_FILEPATH, cache_dir=dir_path
        )
        _ = diffusion_gen.get_model_if_needed(
            diffusion_gen.DIT_MODEL_FILEPATH, cache_dir=dir_path
        )
        print("Finish downloading models")

    def generate(self, work_item):
        for item in work_item:

            # perform upsampling with multiple seeds
            for seed in seeds:
                diffusion_inference_config = diffusion_gen.DiffusionGenerationConfig(
                    audio=semantic_codes,
                    lyrics=item["text"],
                    tags=item["tags"],
                    text_cfg_coef=4.0,
                    ctx_cfg_coef=1.0,
                    steps=steps,
                    seed=seed,
                )

                try:
                    upsampled_latents, upsampled_audio = diffusion_gen.offline_generate(
                        semantic_codes, diffusion_inference_config
                    )
                    upsampled_audios.append(upsampled_audio)
                except Exception as e:
                    print(f"Error generating {s3_filepath}: {e}. Skipping...")
                    continue

                # we want to save out
                # 3. mp3 audio file

                with tempfile.TemporaryDirectory() as td:
                    base_filename = f"{item['id']}-{seed}"

                    # save audio to s3
                    upsampled_audio_path = os.path.join(td, f"{base_filename}.mp3")
                    upsampled_audio.write_hq_mp3(upsampled_audio_path)

                    s3_client.upload_file(
                        upsampled_audio_path,
                        self.s3_bucket,
                        f"{self.encode_s3_dir}/{item_id}/{base_filename}.mp3",
                        ExtraArgs={
                            "ContentType": "audio/mp3",
                        },
                    )


def download_model_wrapper_d():
    # this print is necessary to have modal rerun this when MODEL changes
    # Modal tracks referenced global variables
    # Change the name of the function to force a rerun
    print("Downloading model for history encoder")
    GenerateWorker.download_models()


image = base_image.run_function(download_model_wrapper_d, secrets=SECRETS)
APP_NAME = f"batch-generate-gpu"
app = modal.App(APP_NAME, image=image)

N_MAX_REPLICAS = 64


@app.cls(
    gpu=modal.gpu.A10G(count=1),
    cpu=4,
    secrets=SECRETS,
    timeout=2 * 60 * 60,
    container_idle_timeout=240,
    mounts=MODAL_MOUNTS,
    memory=15000,
    concurrency_limit=N_MAX_REPLICAS,
)
class GenerateStub:
    def __init__(self, gpt_ckpt: str, diff_ckpt: str, output_path: str):
        import torch

        num_gpus = torch.cuda.device_count()
        print(f"Found {num_gpus} GPUs.")
        self.worker = GenerateWorker(gpt_ckpt, diff_ckpt, output_path)

    @modal.method()
    def generate(self, work_item: list[dict]):
        return self.worker.generate(work_item)


@app.local_entrypoint()
def main():
    # checkpoints in s3://suno-data/christian/checkpoints
    # prompts in s3://suno-data/christian/prompts
    # outputs in s3://suno-data/christian/outputs

    # gpt & diffusion checkpoints to evaluate, need to move ckpts to s3 first
    gpt_ckpt = (
        "s3://suno-data/christian/checkpoints/025-02-02_21-04-52-last_ckpt_infer.pt"
    )
    diff_ckpt = "s3://suno-data/tony/tmp/diff/dit_v3_dpo_t10_3k_5e6_b100_t25.pt"

    # load the prompts to evaluate
    prompts_path = "s3://suno-data/christian/prompts/01_genre_prompts.csv"
    # prompts_path = "s3://suno-data/christian/prompts/02_singing_prompts.csv"

    # load the prompts to evaluate
    prompts = pd.read_csv(prompts_path)
    print(prompts.head())
    print(len(prompts))

    chunksize = 32  # num of prompts per worker
    work_items = list(funcy.chunks(chunksize, prompts))
    print(len(work_items), "work items")

    worker = GenerateStub(gpt_ckpt, diff_ckpt, "s3://suno-data/christian/outputs")

    # print("Testing inference...")
    # t0 = time.time()
    # for work_item in work_items[:1]:
    #     _ = worker.upsample.remote(work_item)
    # print(f"{int(round(time.time()-t0))}s for test")

    print("Running batch inference...")
    t0 = time.time()
    _ = list(worker.generate.map(work_items))
    print(round((time.time() - t0) / 60 / 60), "h total for batch embed")
