from suno_utils.utils.text import read_jsonl
import soundfile as sf
import pyloudnorm as pyln
import csv
import os
from joblib import Parallel, delayed
from tqdm import tqdm


"""
This script backfills the loudness metadata from a metas file on the local disk. 
We will iterate over the metas file, find all the local paths, load the audio files, calculate the loudness, also look for bad audio. 
Then we will create a new metadata file that is a dict lookup from id to loudness metadata.
This can then be merged in with existing metadata files or loaded on the side. 
We will use pyloudnorm to calculate the loudness, and we will use soundfile to load the audio files. 
We need to support both opus and mp3 files.
"""


def read_audio_and_calculate_loudness(meta_entry: dict) -> dict:
    local_filepath = meta_entry["local_filepath"]
    file_id = meta_entry["id"]  # get id directly from the meta entry

    result = {
        "id": file_id,
        "local_filepath": local_filepath,
        "lufs_db": None,
        "failed": False,
        "duration_s": None,
        "sample_rate": None,
    }
    try:
        audio, sr = sf.read(local_filepath)
        result["duration_s"] = float(audio.shape[0] / sr)
        result["sample_rate"] = sr
    except Exception as e:
        print(f"Error reading audio file {local_filepath}: {e}")
        result["failed"] = True
        return result

    # create a meter and measure the loudness
    try:
        meter = pyln.Meter(sr)
        # ensure the audio is stereo
        lufs_db = meter.integrated_loudness(audio)
        result["lufs_db"] = lufs_db
    except Exception as e:
        print(f"Error calculating loudness for audio file {local_filepath}: {e}")
        result["failed"] = True
        return result

    return result


def get_existing_ids(results_filepath: str) -> set:
    """Read existing CSV file and return set of already processed IDs"""
    existing_ids = set()
    if os.path.exists(results_filepath):
        try:
            with open(results_filepath, "r", newline="") as f:
                reader = csv.DictReader(f)
                for row in reader:
                    existing_ids.add(row["id"])
            print(
                f"Found {len(existing_ids)} already processed files in {results_filepath}"
            )
        except Exception as e:
            print(
                f"Warning: Could not read existing results file {results_filepath}: {e}"
            )
            print("Starting fresh...")
    else:
        print(
            f"No existing results file found at {results_filepath}, starting fresh..."
        )
    return existing_ids


def main(metas_filepath: str, results_filepath: str, batch_size: int = 10000):
    metas = read_jsonl(metas_filepath)
    print(f"Total metas: {len(metas)}")

    # Get already processed IDs for resumability
    existing_ids = get_existing_ids(results_filepath)

    # Filter out already processed metas
    metas_to_process = [meta for meta in metas if meta["id"] not in existing_ids]
    print(f"Already processed: {len(existing_ids)}")
    print(f"Remaining to process: {len(metas_to_process)}")

    if len(metas_to_process) == 0:
        print("All files have already been processed!")
        return

    failed_count = 0
    csv_fieldnames = [
        "id",
        "local_filepath",
        "lufs_db",
        "failed",
        "duration_s",
        "sample_rate",
    ]

    # Write CSV header only if file doesn't exist
    if not os.path.exists(results_filepath):
        with open(results_filepath, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=csv_fieldnames)
            writer.writeheader()

    # Process in batches to manage memory
    for i in range(0, len(metas_to_process), batch_size):
        batch = metas_to_process[i : i + batch_size]
        batch_num = i // batch_size + 1
        total_batches = (len(metas_to_process) - 1) // batch_size + 1
        print(f"Processing batch {batch_num}/{total_batches} ({len(batch)} files)")

        # Use tqdm with the batch to provide progress for each file
        batch_results = Parallel(n_jobs=-1, backend="loky")(
            delayed(read_audio_and_calculate_loudness)(meta)
            for meta in tqdm(batch, desc=f"Batch {batch_num}", unit="file")
        )

        # Append results to CSV
        with open(results_filepath, "a", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=csv_fieldnames)
            for result in batch_results:
                writer.writerow(result)
                if result["failed"]:
                    failed_count += 1

        print(
            f"Batch {batch_num} complete. Failed: {sum(1 for r in batch_results if r['failed'])}/{len(batch)}"
        )

    print(f"Total processed in this run: {len(metas_to_process)}")
    print(f"Total failed in this run: {failed_count}/{len(metas_to_process)}")
    print(f"Total files in dataset: {len(metas)}")
    print(
        f"Total completed (including previous runs): {len(existing_ids) + len(metas_to_process) - failed_count}"
    )
    if batch_results:
        print("Sample result:", batch_results[0])


if __name__ == "__main__":
    metas_filepath = "/app2/suno/data/diffusion/v1/metas_diff_v0_tr.jsonl"
    results_filepath = "/app2/suno/data/diffusion/v1/metas_diff_v0_tr_loudness.csv"
    main(metas_filepath, results_filepath)
