# modal_wheel_builder.py
import os
from pathlib import Path

import modal
from suno_utils.worker.modal_base import get_modal_base_image_diffusion_with_flash_attention

# Define the Modal volume to store the wheels
model_store_volume = modal.Volume.from_name("suno-wheels", create_if_missing=False)

# Define the container image with CUDA support and build dependencies
base_image = (
    modal.Image.from_registry("nvidia/cuda:12.8.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",
        ]
    )
    .pip_install("torch==2.7.0", "torchaudio==2.7.0", index_url="https://download.pytorch.org/whl/cu128")
    .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",
    )
    .add_local_python_source("suno_utils", copy=False)
)

base_image_with_flash_attention = (
    modal.Image.from_registry("nvidia/cuda:12.8.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",
        ]
    )
    .pip_install("torch==2.7.0", "torchaudio==2.7.0")
    .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("flash-attention")
)

# base_image = get_modal_base_image_diffusion_with_flash_attention()


APP_NAME = "wheel-builder"
# Create the Modal stub
app = modal.App(APP_NAME, image=base_image)


@app.function(
    image=base_image,
    cpu=64,
    memory=344064,
    volumes={"/wheels": model_store_volume},
    timeout=72000,
)
def build_flash_attention_wheel(cuda_version="12.8", torch_version="2.7.0", python_version="3.10"):
    """
    Build Flash Attention 3 as a wheel and save it to a Modal volume.

    Args:
        cuda_version (str): CUDA version (e.g., "12.4")
        torch_version (str): PyTorch version (e.g., "2.5.1")
        python_version (str): Python version (e.g., "3.10")

    Returns:
        str: Path to the built wheel in the volume
    """
    import subprocess
    import glob
    import shutil
    import sys

    # Debug Python version
    print(f"Python version in container: {sys.version}")
    print(f"Python executable: {sys.executable}")
    print(f"Python version info: {sys.version_info}")

    # Verify we're using the expected Python version
    actual_python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
    print(f"Expected Python version: {python_version}")
    print(f"Actual Python version: {actual_python_version}")

    if actual_python_version != python_version:
        print(
            f"WARNING: Python version mismatch! Expected {python_version}, got {actual_python_version}"
        )

    wheels_dir = Path("/wheels")

    # Create a unique directory for this build
    build_id = f"flash-attention-3-cuda{cuda_version}-torch{torch_version}-py{python_version}"
    build_dir = wheels_dir / build_id
    build_dir.mkdir(exist_ok=True)
    print("Installing build dependencies...")
    subprocess.run("pip install build wheel setuptools ninja", shell=True, check=True)

    # Clone the Flash Attention repository
    print("Cloning Flash Attention repository...")
    subprocess.run("git clone https://github.com/Dao-AILab/flash-attention.git", shell=True, check=True)

    os.chdir("flash-attention")

    # Checkout the specific commit before py_limited_api was introduced
    target_commit = "6ba57efea94c5a63cfd17d25a94e47b4065568a4"
    print(f"Checking out commit {target_commit} (before py_limited_api was added)...")
    subprocess.run(f"git checkout {target_commit}", shell=True, check=True)

    # Verify the commit
    result = subprocess.run("git rev-parse HEAD", shell=True, capture_output=True, text=True, check=True)
    current_commit = result.stdout.strip()
    print(f"Current commit: {current_commit}")

    if current_commit != target_commit:
        print(f"WARNING: Expected {target_commit}, got {current_commit}")
    else:
        print("✅ Successfully checked out the correct commit")

    os.chdir("hopper")

    # Check if there's a setup.py or pyproject.toml that might be setting the Python tags
    print("Checking build configuration files...")
    if os.path.exists("setup.py"):
        print("Found setup.py")
        with open("setup.py", "r") as f:
            setup_content = f.read()
            if "python_requires" in setup_content:
                print("setup.py contains python_requires")
            if "py_limited_api" in setup_content:
                print("⚠️ setup.py contains py_limited_api (this should NOT be present in this commit)")
            else:
                print("✅ No py_limited_api found - should build proper cp310 wheels")

    if os.path.exists("pyproject.toml"):
        print("Found pyproject.toml")
        with open("pyproject.toml", "r") as f:
            pyproject_content = f.read()
            print("pyproject.toml content preview:")
            print(pyproject_content[:500])

    # Try building with explicit Python tag control
    env = os.environ.copy()
    env["MAX_JOBS"] = "16"

    # Force the specific Python version tag
    env["SETUPTOOLS_SCM_PRETEND_VERSION"] = "3.0.0b1"

    print("Building Flash Attention wheel...")
    print("Note: Flash Attention 3 may use cp39-abi3 tags for cross-Python compatibility")

    # Use standard pip wheel build - let Flash Attention decide the tags
    subprocess.run(
        ["pip", "wheel", "-v", "--no-deps", "--wheel-dir", str(build_dir), "."], check=True, env=env
    )

    # Alternative modern build method (commented out, but available):
    # subprocess.run([
    #     "python", "-m", "build",
    #     "--wheel",
    #     "--outdir", str(build_dir)
    # ], check=True, env=env)

    # Find the built wheel
    wheel_files = glob.glob(str(build_dir / "*.whl"))
    if not wheel_files:
        # Fallback to dist directory
        wheel_files = glob.glob("dist/*.whl")

    if not wheel_files:
        raise FileNotFoundError("No wheel file found after build")

    wheel_file = wheel_files[0]
    wheel_filename = os.path.basename(wheel_file)

    print(f"Built wheel: {wheel_filename}")

    # Verify the wheel is compatible with current Python version
    print(f"Verifying wheel compatibility with Python {actual_python_version}...")
    try:
        # Install the wheel temporarily to test compatibility
        subprocess.run(["pip", "install", wheel_file], check=True)
        print("✅ Wheel is compatible with current Python version")

        # Try importing to verify it works
        try:
            import flash_attn_interface

            print("✅ Flash attention imported successfully")
        except ImportError as e:
            print(f"⚠️ Flash attention import failed: {e}")

        # Uninstall for cleanup
        subprocess.run(["pip", "uninstall", "-y", "flash-attn"], check=False)

    except subprocess.CalledProcessError as e:
        print(f"❌ Wheel compatibility test failed: {e}")

    # Copy wheel to mounted volume (if not already there)
    if not wheel_file.startswith(str(build_dir)):
        target_path = build_dir / wheel_filename
        shutil.copy(wheel_file, target_path)
        print(f"Copied wheel to: {target_path}")

    # List all wheels in the volume
    saved_wheels = os.listdir("/wheels")

    return {"hopper_wheel": wheel_filename, "all_saved_wheels": saved_wheels}


@app.function(
    image=base_image,
    cpu=16,
    memory=344064,
    gpu="H100",
    volumes={"/wheels": model_store_volume},
    timeout=72000,
)
def install_and_test_flash_attention_wheel(
    wheel_filename="flash_attn_3-3.0.0b1-cp310-cp310-linux_x86_64.whl",
):
    """
    Install the Flash Attention wheel from the volume and test it.

    Args:
        wheel_filename (str): The filename of the wheel in the volume

    Returns:
        dict: Test results and version info
    """
    import subprocess
    import os

    # Path to the wheel in the volume
    wheel_path = f"/wheels/flash-attention-3-cuda12.6-torch2.7.0-py3.10/flash_attn_3-3.0.0b1-cp310-cp310-linux_x86_64.whl"
    # wheel_path = f"/wheels/flash-attention-3-cuda12.4-torch2.5.1-py3.10/{wheel_filename}"

    # Check if wheel exists
    if not os.path.exists(wheel_path):
        available_wheels = os.listdir("/wheels")
        return {"error": f"Wheel {wheel_filename} not found", "available_wheels": available_wheels}

    print(f"Installing wheel from: {wheel_path}")

    # Install the wheel
    try:
        subprocess.run(["pip", "install", wheel_path], check=True, capture_output=True, text=True)
        print("Wheel installed successfully!")
    except subprocess.CalledProcessError as e:
        return {"error": "Failed to install wheel", "stdout": e.stdout, "stderr": e.stderr}

    # Test the installation
    try:
        print("Importing flash_attn")
        import time
        # while True:
        #     time.sleep(1)

        # # Try to get version info
        # version = getattr(flash_attn_interface, "__version__", "unknown")
        version = "3.0.0b1"  # Set default version for the wheel

        # Try a basic functionality test
        import torch

        if torch.cuda.is_available():
            print(f"CUDA available: {torch.cuda.is_available()}")
            print(f"CUDA device count: {torch.cuda.device_count()}")

            # Test basic flash attention functionality
            try:
                from flash_attn_interface import flash_attn_with_kvcache

                print("flash_attn_with_kvcache imported successfully")

                # Create small test tensors
                batch_size, seq_len, num_heads, head_dim = 1, 16, 8, 64
                q = torch.randn(
                    batch_size, seq_len, num_heads, head_dim, dtype=torch.float16, device="cuda"
                )
                k = torch.randn(
                    batch_size, seq_len, num_heads, head_dim, dtype=torch.float16, device="cuda"
                )
                v = torch.randn(
                    batch_size, seq_len, num_heads, head_dim, dtype=torch.float16, device="cuda"
                )

                # Run flash attention
                output = flash_attn_with_kvcache(q, k, v)
                print(f"Flash attention test successful! Output shape: {output.shape}")

                return {
                    "success": True,
                    "version": version,
                    "cuda_available": True,
                    "cuda_device_count": torch.cuda.device_count(),
                    "test_output_shape": list(output.shape),
                }

            except Exception as e:
                return {
                    "success": True,
                    "version": version,
                    "cuda_available": True,
                    "cuda_device_count": torch.cuda.device_count(),
                    "flash_attn_test_error": str(e),
                }
        else:
            return {
                "success": True,
                "version": version,
                "cuda_available": False,
                "note": "CUDA not available for functional testing",
            }

    except ImportError as e:
        return {"error": "Failed to import flash_attn after installation", "import_error": str(e)}
    except Exception as e:
        return {"error": "Failed to install and test flash_attn", "exception": str(e)}


@app.function(
    image=base_image_with_flash_attention,  # Use the image that already has flash-attention
    cpu=4,
    memory=8192,
    timeout=300,
)
def test_flash_attention_wheel():
    """Test the flash-attention installation that's already in the image"""
    try:
        import flash_attn

        print(f"flash_attn: {flash_attn}")

        version = getattr(flash_attn, "__version__", "unknown")
        print(f"Flash Attention version: {version}")

        import torch

        print(f"CUDA available: {torch.cuda.is_available()}")

        if torch.cuda.is_available():
            from flash_attn import flash_attn_func

            print("flash_attn_func imported successfully")

        return {"success": True, "version": version, "cuda_available": torch.cuda.is_available()}

    except ImportError as e:
        return {"error": "Failed to import flash_attn", "import_error": str(e)}


@app.local_entrypoint()
def main():
    # Test installing and using the wheel from the volume
    print("Testing wheel installation from volume...")
    test_result = install_and_test_flash_attention_wheel.remote()
    print(f"Test result: {test_result}")

    # Also test the pre-installed version
    print("\nTesting pre-installed flash-attention...")
    preinstalled_result = test_flash_attention_wheel.remote()
    print(f"Pre-installed test result: {preinstalled_result}")
