import os
import time
import modal
import torch
import funcy
import json
import random
import tempfile
import torchaudio
import numpy as np
import scipy.signal as signal
import pyloudnorm as pyln
from typing import Tuple

from suno_utils.audio import Audio
from suno_utils.worker.settings import s3_client
from suno_utils.worker.modal_base import MODAL_MOUNTS
from suno_utils.utils.text import read_jsonl

from suno_utils.tasks.mert_25 import (
    preload_models as preload_semantic_models,
    encode as encode_semantic,
)

from suno_utils.tasks.dac_vae_fixed_25hz import (
    preload_models as preload_codec_models,
    decode as codec_decode,
    encode as codec_encode,
    decode_stream_to_full_audio,
)


def biquad(
    gain_db: float,
    cutoff_freq: float,
    q_factor: float,
    sample_rate: float,
    filter_type: str,
) -> Tuple[np.ndarray, np.ndarray]:
    """Use design parameters to generate coefficients for a specific filter type."""
    A = 10 ** (gain_db / 40.0)
    w0 = 2.0 * np.pi * (cutoff_freq / sample_rate)
    alpha = np.sin(w0) / (2.0 * q_factor)
    cos_w0 = np.cos(w0)
    sqrt_A = np.sqrt(A)

    if filter_type == "high_shelf":
        b0 = A * ((A + 1) + (A - 1) * cos_w0 + 2 * sqrt_A * alpha)
        b1 = -2 * A * ((A - 1) + (A + 1) * cos_w0)
        b2 = A * ((A + 1) + (A - 1) * cos_w0 - 2 * sqrt_A * alpha)
        a0 = (A + 1) - (A - 1) * cos_w0 + 2 * sqrt_A * alpha
        a1 = 2 * ((A - 1) - (A + 1) * cos_w0)
        a2 = (A + 1) - (A - 1) * cos_w0 - 2 * sqrt_A * alpha
    elif filter_type == "low_shelf":
        b0 = A * ((A + 1) - (A - 1) * cos_w0 + 2 * sqrt_A * alpha)
        b1 = 2 * A * ((A - 1) - (A + 1) * cos_w0)
        b2 = A * ((A + 1) - (A - 1) * cos_w0 - 2 * sqrt_A * alpha)
        a0 = (A + 1) + (A - 1) * cos_w0 + 2 * sqrt_A * alpha
        a1 = -2 * ((A - 1) + (A + 1) * cos_w0)
        a2 = (A + 1) + (A - 1) * cos_w0 - 2 * sqrt_A * alpha
    elif filter_type == "peaking":
        b0 = 1 + alpha * A
        b1 = -2 * cos_w0
        b2 = 1 - alpha * A
        a0 = 1 + alpha / A
        a1 = -2 * cos_w0
        a2 = 1 - alpha / A

    b = np.array([b0, b1, b2]) / a0
    a = np.array([1.0, a1 / a0, a2 / a0])
    return b, a


def apply_stereo_to_mono(audio: torch.Tensor, sample_rate: float):
    return audio.mean(dim=0, keepdims=True).repeat(2, 1)


def apply_channel_imbalance(
    audio: torch.Tensor, sample_rate: float, imbalance: float = 0.0
):
    if not -1 <= imbalance <= 1 or audio.shape[-2] != 2:
        raise ValueError("Invalid input")
    out = audio.clone()
    l_gain, r_gain = (1.0 - imbalance, 1.0) if imbalance > 0 else (1.0, 1.0 + imbalance)
    out[0, :], out[1, :] = out[0, :] * l_gain, out[1, :] * r_gain
    return out


def apply_highpass(audio: torch.Tensor, sample_rate: float, cutoff_hz: float = 1000.0):
    return torchaudio.functional.highpass_biquad(audio, sample_rate, cutoff_hz)


def apply_lowpass(audio: torch.Tensor, sample_rate: float, cutoff_hz: float = 1000.0):
    return torchaudio.functional.lowpass_biquad(audio, sample_rate, cutoff_hz)


def apply_noise(
    audio: torch.Tensor,
    sample_rate: float,
    gain_db: float = 0.0,
    noise_type: str = "white",
):
    gain_lin = 10 ** (gain_db / 20.0)
    noise = torch.randn_like(audio)

    if noise_type == "white":
        return audio + gain_lin * noise
    elif noise_type == "pink":
        b = torch.tensor([0.049922035, -0.095993537, 0.050612699, -0.004408786])
        a = torch.tensor([1, -2.494956002, 2.017265875, -0.522189400])
        noise = torchaudio.functional.filtfilt(noise, a, b)
        noise /= noise.abs().max()
        return audio + gain_lin * noise
    else:
        raise ValueError(f"Invalid noise type: {noise_type}")


def apply_shelving_filter(
    audio: torch.Tensor,
    sample_rate: float,
    gain_db: float,
    cutoff_freq: float,
    q_factor: float,
    filter_type: str,
):
    # convert x to numpy
    audio = audio.numpy()
    b, a = biquad(
        gain_db,
        cutoff_freq,
        q_factor,
        sample_rate,
        filter_type,
    )
    x = signal.lfilter(b, a, audio).astype(np.float32)
    return torch.from_numpy(x)


# randomized corrputions


def apply_random_noise(audio: torch.Tensor, sample_rate: float):
    noise_type = random.choice(["white", "pink"])
    if noise_type == "white":
        noise_gain = random.uniform(-96, -48)
    else:
        noise_gain = random.uniform(-48, -12)
    return apply_noise(audio, sample_rate, noise_gain, noise_type)


def apply_random_stereo_to_mono(audio: torch.Tensor, sample_rate: float):
    return apply_stereo_to_mono(audio, sample_rate)


def apply_random_channel_imbalance(audio: torch.Tensor, sample_rate: float):
    imbalance = random.uniform(-1.0, 1.0)
    return apply_channel_imbalance(audio, sample_rate, imbalance)


def apply_random_filter(audio: torch.Tensor, sample_rate: float):
    filter_type = random.choice(["highpass", "lowpass", "high_shelf", "low_shelf"])
    if filter_type == "highpass":
        cutoff_freq = random.uniform(20, 4000)
        return apply_highpass(audio, sample_rate, cutoff_freq)
    elif filter_type == "lowpass":
        cutoff_freq = random.uniform(1000, 16000)
        return apply_lowpass(audio, sample_rate, cutoff_freq)
    else:
        gain_db = random.uniform(-12, 12)
        if filter_type == "high_shelf":
            cutoff_freq = random.uniform(6000, 20000)
        else:
            cutoff_freq = random.uniform(20, 2000)
        q_factor = random.uniform(0.1, 10.0)
        return apply_shelving_filter(
            audio, sample_rate, gain_db, cutoff_freq, q_factor, filter_type
        )


def corrupt(waveform_tensor, sample_rate):
    """
    Apply a random number of corruptions (at least one) to the input waveform.
    """
    # List of available random corruption functions
    corruption_fns = [
        apply_random_noise,
        apply_random_stereo_to_mono,
        apply_random_channel_imbalance,
        apply_random_filter,
    ]
    n_corr = random.randint(1, len(corruption_fns))  # at least one
    selected = random.sample(corruption_fns, n_corr)
    print(selected)
    out = waveform_tensor.clone()
    for fn in selected:
        out = fn(out, sample_rate)
    return torch.tanh(out)


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.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.10")
    .apt_install(
        "curl", "ffmpeg", "sox", "unzip", "libsox-fmt-mp3", "zlib1g-dev", "git", "clang"
    )
    .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.5.1", "torchaudio==2.5.1")
    .pip_install(
        "flashinfer-python", index_url="https://flashinfer.ai/whl/cu124/torch2.5/"
    )
    .pip_install_private_repos(
        "github.com/suno-ai/glockenspiel.git@a5dba4e50#subdirectory=descript-audio-codec&egg=descript-audio-codec",
        git_user="mcamac",
        secrets=[modal.Secret.from_name("victor-modal-github-token")],
    )
    .pip_install_private_repos(
        "github.com/suno-ai/neon.git@1c83548#subdirectory=hoot",
        git_user="mcamac",
        secrets=[modal.Secret.from_name("victor-modal-github-token")],
    )
    .pip_install(
        "boto3",
        "transformers",
        "tokenizers",
        "encodec",
        "ctc_segmentation",
        "psutil",
        "redis",
        "pydantic",
        "nnAudio",
        "rpyc",
        "biopython>=1.81",  # TODO: don't love this depdendency, for hoot
        "pynvml",  # for torch cuda utilization
        "torchsde",
        "ninja",
        "wheel",
    )
    .pip_install_from_pyproject(
        "/home/christian/code/glockenspiel/suno_utils/pyproject.toml",
    )
    .run_commands(  # This is really slow
        "git clone https://github.com/Dao-AILab/flash-attention.git",
        "cd flash-attention/hopper && python setup.py install",
        gpu="h100",
    )
    .apt_install(
        "libogg0",
        "libopus0",
        "opus-tools",
    )
    .pip_install("transformers==4.44.0", "wandb")
)


class GenerateWorker:
    def __init__(
        self,
        output_path: str,
    ):
        self.output_path = output_path
        start_time = time.time()
        print("Start loading models")
        num_gpus = torch.cuda.device_count()
        cuda_device = torch.cuda.current_device()
        print(f"Found {num_gpus} GPUs. Using GPU {cuda_device}.")

        semantic_model_filepath = "s3://suno-data/georg/models/semantic/mert_25.pt"
        semantic_clusters_filepath = (
            "s3://suno-data/georg/models/semantic/mert_25_2x4k.npy"
        )
        codec_filepath = CODEC_FILEPATH

        _ = preload_semantic_models(semantic_model_filepath, semantic_clusters_filepath)
        _ = preload_codec_models(codec_filepath)

        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")

        print("Finish downloading models")

    def generate(self, work_items):
        """Generate audio from a work item."""
        for item in work_items:
            item_id = item["id"]

            s3_filepath = item["s3_filepath"]
            # read audio from s3 and then semantic encode
            audio = Audio.from_s3(s3_filepath, n_channels=2)

            # also encode semantic codes
            print("Encoding semantic codes...")
            codes = encode_semantic(
                audio.convert(sample_rate=24_000, byte_width=2, n_channels=1)
            ).astype(np.int64)
            print("semantic codes shape:", codes.shape)

            audio = audio.convert(
                sample_rate=48_000, byte_width=2, n_channels=2
            ).normalize_volume(target_db=-16)
            print("audio shape:", audio.array_float.shape)

            # corrupt the audio
            audio_corrupted = corrupt(
                torch.from_numpy(audio.array_float), audio.sample_rate
            )
            audio_corrupted_obj = Audio.from_array_float(
                audio_corrupted.numpy(), audio.sample_rate
            )
            print("audio corrupted shape:", audio_corrupted_obj.array_float.shape)

            # encode the vae latents
            print("Encoding vae latents...")
            vae_latents = codec_encode(audio)
            vae_latents_corrupted = codec_encode(audio_corrupted_obj)

            print("vae latents shape:", vae_latents.shape)
            print("vae latents corrupted shape:", vae_latents_corrupted.shape)

            with tempfile.TemporaryDirectory() as td:
                vae_latents_path = os.path.join(td, f"{item_id}_vae.npz")
                np.savez(vae_latents_path, vae_latents=vae_latents)

                s3_filepath = os.path.join(
                    self.output_path,
                    f"{item_id}_vae.npz",
                )
                s3_client.upload_file(
                    vae_latents_path,
                    "suno-data",
                    s3_filepath,
                    ExtraArgs={"ContentType": "application/octet-stream"},
                )

                vae_latents_corrupted_path = os.path.join(
                    td, f"{item_id}_corrupted_vae.npz"
                )
                np.savez(vae_latents_corrupted_path, vae_latents=vae_latents_corrupted)

                s3_filepath = os.path.join(
                    self.output_path,
                    f"{item_id}_corrupted_vae.npz",
                )
                s3_client.upload_file(
                    vae_latents_corrupted_path,
                    "suno-data",
                    s3_filepath,
                    ExtraArgs={"ContentType": "application/octet-stream"},
                )

                codes_path = os.path.join(td, f"{item_id}_semantic.npz")
                np.savez(codes_path, codes=codes)

                s3_filepath = os.path.join(
                    self.output_path,
                    f"{item_id}_semantic.npz",
                )
                s3_client.upload_file(
                    codes_path,
                    "suno-data",
                    s3_filepath,
                    ExtraArgs={"ContentType": "application/octet-stream"},
                )


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-corrupt-gpu"
app = modal.App(APP_NAME, image=image, secrets=SECRETS)

N_MAX_REPLICAS = 32
GPU_TYPE = "A10G"
CODEC_FILEPATH = "s3://suno-data/minz/models/dac_vae_tuned_25hz.pth"


@app.cls(
    gpu=modal.gpu.A10G(count=1) if GPU_TYPE == "A10G" else modal.gpu.H100(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, output_path: str):
        import torch

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

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


@app.local_entrypoint()
def main():
    work_items = read_jsonl(
        "/home/christian/code/christian/metadata/ear/genius_t6_sampled_10k.jsonl"
    )

    print(f"Total work items: {len(work_items)}")
    chunksize = 128  # num of prompts per worker
    work_items = list(funcy.chunks(chunksize, work_items))
    print(f"Chunksize: {chunksize}, total chunks: {len(work_items)}")

    worker = GenerateStub(f"christian/outputs/corrupt/genius_t6_sampled_10k")

    # print("Testing inference...")
    # t0 = time.time()
    # for work_item in work_items[:1]:
    #    _ = worker.generate.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 generation")
