# Copyright Modal Labs 2024

import json
import os
from pathlib import Path
from typing import List, Dict, Any

import modal

# Create the Modal image with required dependencies
image = modal.Image.debian_slim(python_version="3.12").pip_install("requests==2.31.0")

# Create the Modal app
app = modal.App("suno-audio-downloader", image=image)

# Create a Modal Volume to store the downloaded audio files
audio_volume = modal.Volume.from_name(
    "suno-hoot-data",
    create_if_missing=True,
    version=2,  # use VolumeFS2 for much higher write concurrency
)
model_volume = modal.Volume.from_name("suno-hoot-training")
VOLUME_PATH = Path("/audio_data")

with image.imports():
    import requests


@app.function(
    volumes={VOLUME_PATH: audio_volume},
    timeout=3600,  # 1 hour timeout for large downloads
    cpu=2,
    memory=4096,
)
def download_audio_file_from_item(item: Dict[str, Any]) -> str:
    """Download a single audio file from a JSON item."""

    # Look for URL in various possible fields
    raw_url = (
        item.get("url", "") or item.get("audio_url", "") or item.get("s3_path", "")
    )
    audio_id = item.get("id", "")

    if not raw_url or not audio_id:
        return f"Invalid data for item: {item}"

    # Only accept S3 paths in the specific format
    if raw_url.startswith("s3://suno-data-uploads/studio/uploads/"):
        # Extract filename from S3 path
        filename = raw_url.split("/")[-1]
        url = f"https://cdn1.suno.ai/{filename}"
    else:
        return f"Invalid S3 path format: {raw_url}. Expected format: s3://suno-data-uploads/studio/uploads/filename.mp3"

    # Create local file path
    local_filename = f"{audio_id}.mp3"
    local_file_path = VOLUME_PATH / local_filename
    local_file_path.parent.mkdir(parents=True, exist_ok=True)

    # Download the file
    # print(f"Downloading {url} to {local_file_path}")

    try:
        response = requests.get(url, stream=True, timeout=300)
        response.raise_for_status()

        with open(local_file_path, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

        # Commit the volume to persist the file
        audio_volume.commit()

        return f"Successfully downloaded {local_filename}"

    except requests.RequestException as e:
        return f"Error downloading {url}: {str(e)}"
    except Exception as e:
        return f"Error saving {local_filename}: {str(e)}"


@app.function(
    volumes={VOLUME_PATH: audio_volume},
    timeout=3600,  # 1 hour timeout for large downloads
    cpu=2,
    memory=4096,
)
def download_audio_file(url: str, local_filename: str) -> str:
    """Download a single audio file from URL to the Modal volume."""

    # Create local file path
    local_file_path = VOLUME_PATH / local_filename
    local_file_path.parent.mkdir(parents=True, exist_ok=True)

    # Download the file
    print(f"Downloading {url} to {local_file_path}")

    try:
        response = requests.get(url, stream=True, timeout=300)
        response.raise_for_status()

        with open(local_file_path, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

        # Commit the volume to persist the file
        audio_volume.commit()

        return f"Successfully downloaded {local_filename}"

    except requests.RequestException as e:
        return f"Error downloading {url}: {str(e)}"
    except Exception as e:
        return f"Error saving {local_filename}: {str(e)}"


def process_files_in_batches(
    valid_data: List[Dict[str, Any]], batch_size: int = 1000
) -> List[str]:
    """Process files in batches using Modal's .map API."""

    all_results = []

    # Process files in batches of 1000
    for i in range(0, len(valid_data), batch_size):
        batch = valid_data[i : i + batch_size]
        print(
            f"Processing batch {i // batch_size + 1}/{(len(valid_data) + batch_size - 1) // batch_size} ({len(batch)} files)"
        )

        # Use Modal's .map API to process the batch in parallel
        batch_results = list(download_audio_file_from_item.map(batch))
        all_results.extend(batch_results)

        print(f"Completed batch {i // batch_size + 1}")

    return all_results


@app.function(
    volumes={VOLUME_PATH: audio_volume},
    timeout=600,  # 10 minute timeout
)
def list_downloaded_files() -> List[str]:
    """List all files currently stored in the Modal volume."""

    files = []
    for file_path in VOLUME_PATH.rglob("*"):
        if file_path.is_file():
            files.append(str(file_path.relative_to(VOLUME_PATH)))

    return sorted(files)


@app.function(
    volumes={VOLUME_PATH: audio_volume},
)
def get_volume_stats() -> Dict[str, Any]:
    """Get statistics about the downloaded files."""

    audio_volume.reload()

    total_files = 0
    total_size = 0

    for file_path in VOLUME_PATH.rglob("*.mp3"):
        if file_path.is_file():
            total_files += 1
            total_size += file_path.stat().st_size

    return {
        "total_files": total_files,
        "total_size_bytes": total_size,
        "total_size_mb": round(total_size / (1024 * 1024), 2),
        "total_size_gb": round(total_size / (1024 * 1024 * 1024), 2),
    }


@app.function(
    volumes={VOLUME_PATH: audio_volume, "/models": model_volume},
    timeout=600,  # 10 minute timeout
    cpu=2,
    memory=4096,
)
def create_cleaned_json() -> Dict[str, Any]:
    """Create a cleaned JSON file containing only entries for which there is an actual file in the volume.

    The CDN can throw occational 403s, so we should avoid trying to load files which are not available.

    In production, use an S3 mount to ensure you get all files.
    """

    # Load the original JSON file
    with open("/models/suno_hoot_20250617_subset100k.json", "r") as f:
        original_data = json.load(f)

    print(f"Original JSON contains {len(original_data)} entries")

    # Get all existing audio files in the volume
    existing_files = set()
    for file_path in VOLUME_PATH.rglob("*.mp3"):
        if file_path.is_file():
            # Extract the ID from the filename (remove .mp3 extension)
            file_id = file_path.stem
            existing_files.add(file_id)

    print(f"Found {len(existing_files)} existing audio files in volume")

    # Filter original data to only include entries with existing files
    cleaned_data = []
    for item in original_data:
        item_id = item.get("id", "")
        if item_id in existing_files:
            cleaned_data.append(item)

    print(f"Cleaned JSON will contain {len(cleaned_data)} entries")

    # Write the cleaned JSON file
    with open("/models/suno_hoot_cleaned.json", "w") as f:
        json.dump(cleaned_data, f, indent=2)

    # Commit the model volume to persist the cleaned JSON
    model_volume.commit()

    return {
        "original_entries": len(original_data),
        "existing_files": len(existing_files),
        "cleaned_entries": len(cleaned_data),
        "success_rate": round(len(cleaned_data) / len(original_data) * 100, 2)
        if original_data
        else 0,
    }


@app.function(
    volumes={VOLUME_PATH: audio_volume, "/models": model_volume},
    timeout=14400,  # 4 hour timeout for the full orchestration
    cpu=4,
    memory=8192,
)
def orchestrate():
    """Main orchestration function that runs the entire download process."""

    with open("/models/suno_hoot_20250617_subset100k.json", "r") as f:
        data = json.load(f)

    print(f"Received {len(data)} items for processing")

    # Filter data to only include items with URLs
    valid_data = [
        item
        for item in data
        if item.get("url") or item.get("audio_url") or item.get("s3_path")
    ]
    print(f"Found {len(valid_data)} items with valid URLs")

    if not valid_data:
        print("No valid items found to download")
        return

    # Download files in batches using Modal's .map API
    print("Starting download process...")
    results = process_files_in_batches(valid_data)

    # Print results
    for result in results:
        print(result)

    # Get volume statistics
    stats = get_volume_stats.remote()
    print("\nVolume Statistics:")
    print(f"Total files: {stats['total_files']}")
    print(f"Total size: {stats['total_size_mb']} MB ({stats['total_size_gb']} GB)")

    print("\nDownload process completed!")


@app.local_entrypoint()
def main():
    """Local entrypoint that loads JSON data and runs orchestration in detached mode."""
    print("Starting orchestration...")

    # Run the orchestration function remotely (detached) with the loaded data
    orchestrate.remote()


@app.local_entrypoint()
def clean_json():
    """Local entrypoint to create a cleaned JSON file with only entries that have downloaded files."""
    print("Creating cleaned JSON file...")

    # Run the cleaning function remotely
    result = create_cleaned_json.remote()

    print(f"Cleaning completed:")
    print(f"  Original entries: {result['original_entries']}")
    print(f"  Existing files: {result['existing_files']}")
    print(f"  Cleaned entries: {result['cleaned_entries']}")
    print(f"  Success rate: {result['success_rate']}%")
    print(f"  Cleaned JSON saved to: /models/suno_hoot_cleaned.json")


if __name__ == "__main__":
    main()
