import numpy as np
import torch
import argparse
import json
from pathlib import Path
from typing import List, Optional, Union, Dict, Any
from suno_utils.tasks.mert_25 import encode, preload_models
from suno_utils.audio import Audio
from tqdm import tqdm
import os


def encode_audio_to_npz(
    audio_path: Union[str, Path],
    save_path: Union[str, Path],
    device: Optional[str] = None,
) -> None:
    """
    Encode an audio file using MERT and save the codes to an NPZ file.

    Args:
        audio_path: Path to the input audio file
        save_path: Path to save the output NPZ file
        device: CUDA device string (e.g., 'cuda:0'). If None, will use default device.
    """
    import tempfile
    import shutil

    # Convert Path to str for Audio.from_file
    audio_path = str(audio_path)
    save_path = str(save_path)

    # Load audio file
    audio = Audio.from_file(audio_path)

    # Encode using MERT (it will use the device specified during preload_models)
    # we only keep the first one!
    codes = encode(audio, do_clustering=True, device=device)[:, 0]

    # Ensure codes is a numpy array
    if not isinstance(codes, np.ndarray):
        codes = np.array(codes)

        # Save to a temporary file first, then move atomically
    save_dir = os.path.dirname(save_path)

    # Generate a unique temporary filename without creating the file
    tmp_path = os.path.join(save_dir, f".tmp_{os.getpid()}_{id(codes)}.npz")

    try:
        # Save to temporary file
        np.savez_compressed(tmp_path, codes=codes)

        # Atomically move to final location
        # os.rename is atomic on the same filesystem
        os.rename(tmp_path, save_path)

    except Exception as e:
        # Clean up temporary file if something went wrong
        if os.path.exists(tmp_path):
            os.remove(tmp_path)
        raise e


def process_jsonl_chunk(
    jsonl_path: Union[str, Path],
    output_dir: Union[str, Path],
    chunk_id: int,
    total_chunks: int = 8,
    local_gpu_id: Optional[int] = None,
) -> None:
    """
    Process a chunk of the JSONL file based on chunk_id.

    Args:
        jsonl_path: Path to the JSONL file
        output_dir: Directory to save NPZ files
        chunk_id: Global chunk ID across all nodes
        total_chunks: Total number of chunks to split the data
        local_gpu_id: Local GPU ID on this node (0-7). If None, uses chunk_id % 8
    """
    # Determine local GPU ID
    if local_gpu_id is None:
        local_gpu_id = chunk_id % 8

    # When CUDA_VISIBLE_DEVICES is set, PyTorch sees only the visible GPU(s)
    # and indexes them starting from 0
    if "CUDA_VISIBLE_DEVICES" in os.environ:
        # If CUDA_VISIBLE_DEVICES is set, use device 0 (the only visible device)
        device = "cuda:0"
        device_id = 0
    else:
        # If not set, use the actual GPU ID
        device = f"cuda:{local_gpu_id}"
        device_id = local_gpu_id

    # Check if CUDA is available
    if not torch.cuda.is_available():
        print(f"Warning: CUDA not available, using CPU instead")
        device = "cpu"
    else:
        try:
            torch.cuda.set_device(device_id)
        except RuntimeError as e:
            print(f"Warning: Failed to set CUDA device {device_id}: {e}")
            device = "cpu"

    # Preload models onto the specified device
    print(f"Loading models on {device} for chunk {chunk_id}...")
    preload_models(
        checkpoint_filepath="/app/suno/data/dpo/models/mert_25.pt",
        centroids_filepath="/app/suno/data/dpo/models/mert_25_2x4k.npy",
        device=device,
    )

    # Create output directory if it doesn't exist
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Load JSONL file
    jsonl_path = Path(jsonl_path)
    print(f"Loading JSONL file: {jsonl_path}")

    entries: List[Dict[str, Any]] = []
    try:
        with open(jsonl_path, "r") as f:
            for line in f:
                line = line.strip()
                if line:  # Skip empty lines
                    try:
                        entry = json.loads(line)
                        entries.append(entry)
                    except json.JSONDecodeError as e:
                        print(f"Warning: Failed to parse JSON line: {e}")
                        continue
    except FileNotFoundError:
        print(f"Error: JSONL file not found: {jsonl_path}")
        return
    except Exception as e:
        print(f"Error reading JSONL file: {e}")
        return

    # Calculate chunk boundaries
    total_entries = len(entries)
    if total_entries == 0:
        print("Warning: No entries found in JSONL file")
        return

    chunk_size = total_entries // total_chunks
    start_idx = chunk_id * chunk_size

    # Handle the last chunk to include any remaining entries
    if chunk_id == total_chunks - 1:
        end_idx = total_entries
    else:
        end_idx = (chunk_id + 1) * chunk_size

    chunk_entries = entries[start_idx:end_idx]
    print(
        f"Processing chunk {chunk_id} (GPU {local_gpu_id}): entries {start_idx} to {end_idx-1} (total: {len(chunk_entries)})"
    )

    # Process each entry in the chunk
    successful = 0
    failed = 0

    for entry in tqdm(chunk_entries, desc=f"Chunk {chunk_id}"):
        try:
            # Get the local file path and ID from the entry
            audio_path = entry.get("local_filepath")
            entry_id = entry.get("id")

            if not audio_path:
                print(f"Warning: Missing local_filepath in entry: {entry}")
                failed += 1
                continue

            if not entry_id:
                print(f"Warning: Missing id in entry: {entry}")
                failed += 1
                continue

            # Ensure paths are strings
            audio_path = str(audio_path)
            entry_id = str(entry_id)

            # Check if audio file exists
            if not Path(audio_path).exists():
                print(f"Warning: Audio file not found: {audio_path}")
                failed += 1
                continue

            # Generate output filename using the ID
            output_filename = f"{entry_id}.npz"
            output_path = output_dir / output_filename

            # Skip if already processed
            # if output_path.exists():
            #     successful += 1
            #     continue

            # Process the audio file
            encode_audio_to_npz(audio_path, output_path, device=device)
            successful += 1

        except Exception as e:
            entry_id = entry.get("id", "unknown")
            print(f"Error processing entry {entry_id}: {e}")
            import traceback

            traceback.print_exc()
            failed += 1

    print(f"\nChunk {chunk_id} completed: {successful} successful, {failed} failed")


def main():
    parser = argparse.ArgumentParser(
        description="Encode audio files from JSONL using MERT and save to NPZ"
    )
    parser.add_argument(
        "--jsonl_path",
        type=str,
        default="/app2/suno/data/dpo/sft/sft_metas_tr_v11.jsonl",
        help="Path to the JSONL file containing audio metadata",
    )
    parser.add_argument(
        "--output_dir", type=str, required=True, help="Directory to save NPZ files"
    )
    parser.add_argument(
        "--chunk_id",
        type=int,
        required=True,
        help="Global chunk ID across all nodes",
    )
    parser.add_argument(
        "--total_chunks",
        type=int,
        default=128,
        help="Total number of chunks to split the data",
    )
    parser.add_argument(
        "--local_gpu_id",
        type=int,
        default=None,
        help="Local GPU ID on this node (0-7)",
    )

    args = parser.parse_args()

    # Process the chunk
    process_jsonl_chunk(
        args.jsonl_path,
        args.output_dir,
        args.chunk_id,
        args.total_chunks,
        args.local_gpu_id,
    )


if __name__ == "__main__":
    main()


# Simple test function
def test_encode_audio():
    """
    Simple test case for encoding audio.
    This creates a synthetic audio file for testing.
    """
    import tempfile
    import os

    # Set device
    device = "cuda:0" if torch.cuda.is_available() else "cpu"

    # Preload models
    print("Loading models for test...")
    preload_models(
        checkpoint_filepath="/app/suno/data/dpo/models/mert_25.pt",
        centroids_filepath="/app/suno/data/dpo/models/mert_25_2x4k.npy",
        device=device,
    )

    # Create a test audio file (silence for testing)
    test_audio = Audio.from_silence(duration_s=5.0, sample_rate=24000)

    with tempfile.TemporaryDirectory() as tmpdir:
        # Save test audio
        audio_path = os.path.join(tmpdir, "test_audio.wav")
        test_audio.write_wav(audio_path)

        # Encode and save
        npz_path = os.path.join(tmpdir, "test_encoded.npz")
        encode_audio_to_npz(audio_path, npz_path, device=device)

        # Verify the file was created
        assert os.path.exists(npz_path), "NPZ file was not created"

        # Load and verify the encoded data
        loaded_data = np.load(npz_path, allow_pickle=True)
        codes = loaded_data["codes"]

        # Basic checks
        assert isinstance(codes, np.ndarray), "Codes should be a numpy array"
        assert codes.ndim == 1, f"Codes should be 1D, but got shape {codes.shape}"
        assert codes.shape[0] > 0, "Codes should have non-zero length"

        print(f"Test passed! Encoded shape: {codes.shape}")
        print(f"Codes dtype: {codes.dtype}")
