import os
import pandas as pd
import numpy as np
from tqdm import tqdm
from suno_utils.utils.text import write_jsonl
from joblib import Parallel, delayed
import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time

version = "t13"
output_dir = "/home/christian/code/christian/metadata/reward_model"

dataframes = [
    "/home/tony/Data/Preference/up_v2_d3/interesting_clips_ahi_d3_20250608.pkl",
    "/home/tony/Data/Preference/up_v2_d4/interesting_clips_ahi_d4_20250714.pkl",
    # "/home/tony/Data/Preference/up_v2_d5/interesting_clips_ahi_d5_20250824.pkl"
    "/home/tony/Data/Preference/up_v2_d5/fully_merged_up_v2_d5.pkl",
]

root_dirs = [
    "/app2/suno/data/dpo/diff2_v2_d3",
    "/app2/suno/data/dpo/diff2_v2_d4",
    "/app2/suno/data/dpo/diff2_v2_d5",
]


def process_index(index, df, root_dir):
    pos_item_id = df.iloc[index]["id"]
    neg_item_id = df.iloc[index - 1]["id"]

    pos_vae_latents_filepath = os.path.join(root_dir, f"{pos_item_id}_vae.npz")
    neg_vae_latents_filepath = os.path.join(root_dir, f"{neg_item_id}_vae.npz")

    if os.path.exists(pos_vae_latents_filepath) and os.path.exists(
        neg_vae_latents_filepath
    ):
        return {
            "id": pos_item_id,
            "pos_vae_latents_filepath": pos_vae_latents_filepath,
            "neg_vae_latents_filepath": neg_vae_latents_filepath,
        }
    else:
        return None


def process_index_batch(index_batch, df, root_dir):
    """Process a batch of indices to reduce overhead"""
    results = []
    for index in index_batch:
        pos_item_id = df.iloc[index]["id"]
        neg_item_id = df.iloc[index - 1]["id"]

        pos_vae_latents_filepath = os.path.join(root_dir, f"{pos_item_id}_vae.npz")
        neg_vae_latents_filepath = os.path.join(root_dir, f"{neg_item_id}_vae.npz")

        if os.path.exists(pos_vae_latents_filepath) and os.path.exists(
            neg_vae_latents_filepath
        ):
            results.append(
                {
                    "id": pos_item_id,
                    "pos_vae_latents_filepath": pos_vae_latents_filepath,
                    "neg_vae_latents_filepath": neg_vae_latents_filepath,
                }
            )
    return results


def process_with_threading(indices, df, root_dir, n_jobs=None):
    """Use threading for I/O bound tasks"""
    if n_jobs is None:
        n_jobs = min(32, (os.cpu_count() or 1) + 4)  # More threads for I/O bound

    # Create batches to reduce overhead
    batch_size = max(1, len(indices) // (n_jobs * 4))
    index_batches = [
        indices[i : i + batch_size] for i in range(0, len(indices), batch_size)
    ]

    print(
        f"Using {n_jobs} threads with {len(index_batches)} batches of size {batch_size}"
    )

    with ThreadPoolExecutor(max_workers=n_jobs) as executor:
        futures = []
        for batch in index_batches:
            # Pass the full dataframe - threading shares memory so this is efficient
            future = executor.submit(process_index_batch, batch, df, root_dir)
            futures.append(future)

        results = []
        for future in tqdm(futures, desc="Processing batches"):
            batch_results = future.result()
            results.extend(batch_results)

    return results


def process_with_multiprocessing(indices, df, root_dir, n_jobs=None):
    """Use multiprocessing with better chunking"""
    if n_jobs is None:
        n_jobs = mp.cpu_count()

    # Create larger batches to reduce serialization overhead
    batch_size = max(10, len(indices) // (n_jobs * 2))
    index_batches = [
        indices[i : i + batch_size] for i in range(0, len(indices), batch_size)
    ]

    print(
        f"Using {n_jobs} processes with {len(index_batches)} batches of size {batch_size}"
    )

    with ProcessPoolExecutor(max_workers=n_jobs) as executor:
        futures = []
        for batch in index_batches:
            # Pass the full dataframe to each process
            future = executor.submit(process_index_batch, batch, df, root_dir)
            futures.append(future)

        results = []
        for future in tqdm(futures, desc="Processing batches"):
            batch_results = future.result()
            results.extend(batch_results)

    return results


def process_with_joblib_improved(
    indices, df, root_dir, n_jobs=None, backend="threading"
):
    """Improved joblib implementation with better chunking"""
    if n_jobs is None:
        n_jobs = -1

    # Create batches to reduce overhead
    batch_size = max(5, len(indices) // (abs(n_jobs) * 3))
    index_batches = [
        indices[i : i + batch_size] for i in range(0, len(indices), batch_size)
    ]

    print(
        f"Using joblib with {abs(n_jobs)} workers, {len(index_batches)} batches of size {batch_size}, backend={backend}"
    )

    def process_batch_wrapper(batch):
        return process_index_batch(batch, df, root_dir)

    results = Parallel(n_jobs=n_jobs, backend=backend, batch_size=1)(
        delayed(process_batch_wrapper)(batch)
        for batch in tqdm(index_batches, desc="Processing batches")
    )

    # Flatten results
    flattened_results = []
    for batch_results in results:
        flattened_results.extend(batch_results)

    return flattened_results


if __name__ == "__main__":
    # Configuration - you can change these to test different approaches
    PARALLEL_METHOD = "threading"  # Options: "threading", "multiprocessing", "joblib_threading", "joblib_loky"
    N_JOBS = None  # None for auto-detection, or specify a number

    print(f"CPU count: {mp.cpu_count()}")
    print(f"Using parallel method: {PARALLEL_METHOD}")

    train_metas = []
    val_metas = []

    for root_dir, df_path in zip(root_dirs, dataframes):
        print(f"\nProcessing {df_path}")
        df = pd.read_pickle(df_path)
        print(f"DataFrame size: {len(df)}")

        indices = np.arange(len(df))
        indices = indices[indices % 2 == 1]
        print(f"Processing {len(indices)} indices")

        start_time = time.time()

        # Choose parallel processing method
        if PARALLEL_METHOD == "threading":
            subset_metas = process_with_threading(indices, df, root_dir, N_JOBS)
        elif PARALLEL_METHOD == "multiprocessing":
            subset_metas = process_with_multiprocessing(indices, df, root_dir, N_JOBS)
        elif PARALLEL_METHOD == "joblib_threading":
            subset_metas = process_with_joblib_improved(
                indices, df, root_dir, N_JOBS, "threading"
            )
        elif PARALLEL_METHOD == "joblib_loky":
            subset_metas = process_with_joblib_improved(
                indices, df, root_dir, N_JOBS, "loky"
            )
        else:
            # Fallback to original method
            print("Using original joblib method")
            results = Parallel(n_jobs=-1, backend="loky")(
                delayed(process_index)(index, df, root_dir) for index in tqdm(indices)
            )
            subset_metas = [meta for meta in results if meta is not None]

        end_time = time.time()
        print(
            f"Processed {len(subset_metas)} valid items in {end_time - start_time:.2f} seconds"
        )
        print(f"Rate: {len(subset_metas) / (end_time - start_time):.2f} items/second")

        train_metas.extend(subset_metas[: int(len(subset_metas) * 0.95)])
        val_metas.extend(subset_metas[int(len(subset_metas) * 0.95) :])

    print("\nFinal results:")
    print(f"Train metas: {len(train_metas)}")
    print(f"Val metas: {len(val_metas)}")

    write_jsonl(train_metas, output_dir + f"/metas_tr_{version}.jsonl")
    write_jsonl(val_metas, output_dir + f"/metas_val_{version}.jsonl")
