use std::{
    env, fs,
    path::{Path, PathBuf},
    sync::Arc,
    thread,
    time::{Duration, Instant},
};

use singe_cuda::{
    context::Context as CudaContext, data_type::DataType, device::Device, memory::DeviceMemory,
    stream::StreamBinding, types::Complex32,
};
use singe_cutensor::{
    mp::{
        context::Context as MpContext,
        plan::{OperationDescriptor, Plan, PlanPreference},
        tensor::{RankPartition, TensorDescriptor, TensorDescriptorConfig, TensorMode},
        types::Algorithm,
    },
    operation::ComputeDescriptor,
    types::{Mode, Operator},
};
use singe_nccl::{communicator::Communicator, types::UniqueId, unique_id};

const DEFAULT_EQUATION: &str = "mk,kn->mn";
const DEFAULT_EXTENT: u64 = 2048;
const DEFAULT_REPETITIONS: usize = 5;
const DEFAULT_DEVICE_WORKSPACE_BUDGET: u64 = 10 * 1024 * 1024 * 1024;
const DEFAULT_HOST_WORKSPACE_BUDGET: u64 = 1024;
const UNIQUE_ID_BYTES: usize = 128;

#[derive(Debug)]
struct Config {
    equation: String,
    dist_a: String,
    dist_b: String,
    dist_c: String,
    extent: u64,
    repetitions: usize,
    rank: i32,
    rank_count: i32,
    local_device: Option<i32>,
    id_file: PathBuf,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            equation: DEFAULT_EQUATION.into(),
            dist_a: String::new(),
            dist_b: String::new(),
            dist_c: String::new(),
            extent: DEFAULT_EXTENT,
            repetitions: DEFAULT_REPETITIONS,
            rank: env_parse("SINGE_CUTENSOR_MP_RANK").unwrap_or(0),
            rank_count: env_parse("SINGE_CUTENSOR_MP_NRANKS").unwrap_or(1),
            local_device: env_parse("SINGE_CUTENSOR_MP_LOCAL_DEVICE"),
            id_file: env::var_os("SINGE_CUTENSOR_MP_ID_FILE")
                .map(PathBuf::from)
                .unwrap_or_else(|| env::temp_dir().join("singe-cutensor-mp-nccl-id.bin")),
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = parse_args()?;
    validate_config(&config)?;

    let device_count = Device::count()?;
    let local_device = config
        .local_device
        .unwrap_or_else(|| config.rank.rem_euclid(device_count));

    let cuda_context = CudaContext::retain_primary_for_device(Device::new(local_device))?;
    let stream = cuda_context.create_stream()?;
    let stream_binding = StreamBinding::Borrowed(stream.to_borrowed());

    let communicator = create_communicator(&cuda_context, &config)?;
    let context = MpContext::create(&communicator, local_device, stream_binding)?;

    if config.rank == 0 {
        println!("cuTENSORMp contraction sample");
        println!("rank count: {}", config.rank_count);
        println!("equation: {}", config.equation);
    }

    let [modes_a_chars, modes_b_chars, modes_c_chars] = split_equation(&config.equation)?;
    let modes_a = chars_to_raw_modes(&modes_a_chars);
    let modes_b = chars_to_raw_modes(&modes_b_chars);
    let modes_c = chars_to_raw_modes(&modes_c_chars);
    let cutensor_modes_a = raw_modes_to_cutensor(&modes_a);
    let cutensor_modes_b = raw_modes_to_cutensor(&modes_b);
    let cutensor_modes_c = raw_modes_to_cutensor(&modes_c);

    let extent_a = get_extent(&modes_a, config.extent);
    let extent_b = get_extent(&modes_b, config.extent);
    let extent_c = get_extent(&modes_c, config.extent);

    let distributed_modes_a = build_distributed_modes(&modes_a, &config.dist_a);
    let distributed_modes_b = build_distributed_modes(&modes_b, &config.dist_b);
    let distributed_modes_c = build_distributed_modes(&modes_c, &config.dist_c);

    let ranks_per_mode_a = get_ranks_per_mode(&modes_a, &distributed_modes_a, &extent_a);
    let ranks_per_mode_b = get_ranks_per_mode(&modes_b, &distributed_modes_b, &extent_b);
    let ranks_per_mode_c = get_ranks_per_mode(&modes_c, &distributed_modes_c, &extent_c);

    let local_extent_a = calc_local_extents(&extent_a, &ranks_per_mode_a);
    let local_extent_b = calc_local_extents(&extent_b, &ranks_per_mode_b);
    let local_extent_c = calc_local_extents(&extent_c, &ranks_per_mode_c);

    if config.rank == 0 {
        print_modes("modeA", &modes_a_chars);
        print_modes("modeB", &modes_b_chars);
        print_modes("modeC", &modes_c_chars);
        print_values("extentA", &extent_a);
        print_values("extentB", &extent_b);
        print_values("extentC", &extent_c);
        print_values("nranksPerModeA", &ranks_per_mode_a);
        print_values("nranksPerModeB", &ranks_per_mode_b);
        print_values("nranksPerModeC", &ranks_per_mode_c);
        print_values("localExtentA", &local_extent_a);
        print_values("localExtentB", &local_extent_b);
        print_values("localExtentC", &local_extent_c);
    }
    let tensor_modes_a = extent_a
        .iter()
        .copied()
        .map(TensorMode::contiguous)
        .collect::<Vec<_>>();
    let tensor_modes_b = extent_b
        .iter()
        .copied()
        .map(TensorMode::contiguous)
        .collect::<Vec<_>>();
    let tensor_modes_c = extent_c
        .iter()
        .copied()
        .map(TensorMode::contiguous)
        .collect::<Vec<_>>();

    let descriptor_a = TensorDescriptor::create(
        &context,
        TensorDescriptorConfig {
            modes: &tensor_modes_a,
            partition: RankPartition {
                ranks_per_mode: &ranks_per_mode_a,
                ranks: None,
                rank_count: config.rank_count as u32,
            },
            data_type: DataType::ComplexF32,
        },
    )?;
    let descriptor_b = TensorDescriptor::create(
        &context,
        TensorDescriptorConfig {
            modes: &tensor_modes_b,
            partition: RankPartition {
                ranks_per_mode: &ranks_per_mode_b,
                ranks: None,
                rank_count: config.rank_count as u32,
            },
            data_type: DataType::ComplexF32,
        },
    )?;
    let descriptor_c = TensorDescriptor::create(
        &context,
        TensorDescriptorConfig {
            modes: &tensor_modes_c,
            partition: RankPartition {
                ranks_per_mode: &ranks_per_mode_c,
                ranks: None,
                rank_count: config.rank_count as u32,
            },
            data_type: DataType::ComplexF32,
        },
    )?;

    let operation = OperationDescriptor::create_contraction(
        &context,
        &descriptor_a,
        &cutensor_modes_a,
        Operator::Identity,
        &descriptor_b,
        &cutensor_modes_b,
        Operator::Identity,
        &descriptor_c,
        &cutensor_modes_c,
        Operator::Identity,
        &descriptor_c,
        &cutensor_modes_c,
        ComputeDescriptor::f32(),
    )?;
    let preference = PlanPreference::create(
        &context,
        Algorithm::Default,
        DEFAULT_DEVICE_WORKSPACE_BUDGET,
        DEFAULT_HOST_WORKSPACE_BUDGET,
    )?;
    if config.rank == 0 {
        println!("cuTENSORMp using algorithm {}", preference.algorithm());
    }

    let plan = Plan::create(&context, &operation, Some(&preference))?;
    let workspace = plan.workspace()?;
    if config.rank == 0 {
        println!(
            "workspace: device={} bytes, host={} bytes",
            workspace.device_size(),
            workspace.host_size()
        );
    }
    let mut workspace_memory = plan.create_workspace_memory()?;

    let mut a = DeviceMemory::<Complex32>::create(product_usize(&local_extent_a)?)?;
    let mut b = DeviceMemory::<Complex32>::create(product_usize(&local_extent_b)?)?;
    let mut c = DeviceMemory::<Complex32>::create(product_usize(&local_extent_c)?)?;

    unsafe {
        a.set_value_async_unchecked((config.rank & 0xff) as u8, &stream)?;
        b.set_value_async_unchecked(
            ((config.rank * config.rank_count + config.rank + 1) & 0xff) as u8,
            &stream,
        )?;
    }
    stream.synchronize()?;

    let alpha = Complex32::new(1.0, 0.0);
    let beta = Complex32::new(0.0, 0.0);
    let mut timings = Vec::new();

    for repetition in 0..config.repetitions {
        unsafe {
            c.set_value_async_unchecked(0, &stream)?;
        }
        stream.synchronize()?;

        let start = Instant::now();
        plan.contract_in_place(&alpha, &a, &b, &beta, &mut c, &mut workspace_memory)?;
        stream.synchronize()?;

        if repetition > 0 {
            timings.push(start.elapsed().as_secs_f64() * 1.0e3);
        }
    }

    if config.rank == 0 && !timings.is_empty() {
        let total_ms = timings.iter().sum::<f64>();
        let min_ms = timings.iter().copied().fold(f64::INFINITY, f64::min);
        println!(
            "\ncuTENSORMp - average time: {:.3}ms, min time: {:.3}ms",
            total_ms / timings.len() as f64,
            min_ms
        );
        print_performance_metrics(
            &modes_a,
            &modes_b,
            &modes_c,
            &local_extent_a,
            &local_extent_b,
            &local_extent_c,
            min_ms,
        );
    }

    if config.rank == 0 {
        println!("\ndone: everything completed successfully");
    }
    Ok(())
}

fn parse_args() -> Result<Config, Box<dyn std::error::Error>> {
    let mut config = Config::default();
    let mut args = env::args().skip(1).peekable();

    while let Some(arg) = args.next() {
        let (name, value) = if let Some((name, value)) = arg.split_once('=') {
            (name.to_string(), Some(value.to_string()))
        } else {
            (arg, None)
        };

        match name.as_str() {
            "--eq" => config.equation = next_value(value, &mut args, "--eq")?,
            "--distA" => config.dist_a = next_value(value, &mut args, "--distA")?,
            "--distB" => config.dist_b = next_value(value, &mut args, "--distB")?,
            "--distC" => config.dist_c = next_value(value, &mut args, "--distC")?,
            "--extent" => config.extent = next_value(value, &mut args, "--extent")?.parse()?,
            "--reps" => config.repetitions = next_value(value, &mut args, "--reps")?.parse()?,
            "--rank" => config.rank = next_value(value, &mut args, "--rank")?.parse()?,
            "--nranks" => config.rank_count = next_value(value, &mut args, "--nranks")?.parse()?,
            "--local-device" => {
                config.local_device =
                    Some(next_value(value, &mut args, "--local-device")?.parse()?);
            }
            "--id-file" => {
                config.id_file = PathBuf::from(next_value(value, &mut args, "--id-file")?)
            }
            "--help" | "-h" => {
                print_usage();
                std::process::exit(0);
            }
            other => return Err(format!("unknown argument `{other}`").into()),
        }
    }

    Ok(config)
}

fn next_value(
    inline: Option<String>,
    args: &mut std::iter::Peekable<impl Iterator<Item = String>>,
    name: &str,
) -> Result<String, Box<dyn std::error::Error>> {
    if let Some(value) = inline {
        return Ok(value);
    }
    args.next()
        .ok_or_else(|| format!("missing value for `{name}`").into())
}

fn print_usage() {
    println!(
        "usage: cargo run -p singe-cutensor --example cutensor_mp_contraction -- [options]\n\
         options:\n\
           --eq <einsum>              contraction equation, default `{DEFAULT_EQUATION}`\n\
           --distA <modes>            distributed modes for A\n\
           --distB <modes>            distributed modes for B\n\
           --distC <modes>            distributed modes for C/D\n\
           --extent <n>               uniform extent per mode, default {DEFAULT_EXTENT}\n\
           --reps <n>                 repetitions, default {DEFAULT_REPETITIONS}\n\
           --rank <rank>              NCCL rank, default env SINGE_CUTENSOR_MP_RANK or 0\n\
           --nranks <n>               NCCL rank count, default env SINGE_CUTENSOR_MP_NRANKS or 1\n\
           --local-device <id>        CUDA device id, default rank modulo device count\n\
           --id-file <path>           NCCL unique-id rendezvous file for multi-process runs"
    );
}

fn validate_config(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
    if config.rank_count <= 0 {
        return Err("rank count must be positive".into());
    }
    if config.rank < 0 || config.rank >= config.rank_count {
        return Err("rank must be in [0, rank_count)".into());
    }
    if config.extent == 0 {
        return Err("extent must be positive".into());
    }
    if config.repetitions == 0 {
        return Err("repetitions must be positive".into());
    }
    Ok(())
}

fn create_communicator(
    cuda_context: &Arc<CudaContext>,
    config: &Config,
) -> Result<Communicator, Box<dyn std::error::Error>> {
    let id = if config.rank_count == 1 {
        unique_id()?
    } else {
        unique_id_from_file(config.rank, &config.id_file)?
    };
    Ok(Communicator::create_rank(
        cuda_context,
        config.rank_count,
        id,
        config.rank,
    )?)
}

fn unique_id_from_file(rank: i32, path: &Path) -> Result<UniqueId, Box<dyn std::error::Error>> {
    if rank == 0 {
        let id = unique_id()?;
        fs::write(path, id.as_bytes())?;
        Ok(id)
    } else {
        let deadline = Instant::now() + Duration::from_secs(60);
        loop {
            match fs::read(path) {
                Ok(bytes) if bytes.len() == UNIQUE_ID_BYTES => {
                    let mut array = [0_u8; UNIQUE_ID_BYTES];
                    array.copy_from_slice(&bytes);
                    return Ok(UniqueId::from_bytes(array));
                }
                Ok(_) => {
                    return Err("invalid nccl unique id file length".into());
                }
                Err(error)
                    if error.kind() == std::io::ErrorKind::NotFound
                        && Instant::now() < deadline =>
                {
                    thread::sleep(Duration::from_millis(50));
                }
                Err(error) => return Err(error.into()),
            }
        }
    }
}

fn split_equation(eq: &str) -> Result<[Vec<char>; 3], Box<dyn std::error::Error>> {
    let compact = eq
        .chars()
        .filter(|c| !c.is_whitespace())
        .collect::<String>();
    let Some((lhs, rhs)) = compact.split_once("->") else {
        return Err("equation must contain `->`".into());
    };
    let inputs = lhs.split(',').collect::<Vec<_>>();
    if inputs.len() != 2 || rhs.is_empty() || inputs.iter().any(|input| input.is_empty()) {
        return Err("equation must have form `a,b->c`".into());
    }
    Ok([
        inputs[0].chars().collect(),
        inputs[1].chars().collect(),
        rhs.chars().collect(),
    ])
}

fn chars_to_raw_modes(modes: &[char]) -> Vec<i32> {
    modes.iter().map(|&mode| mode as i32).collect()
}

fn raw_modes_to_cutensor(modes: &[i32]) -> Vec<Mode> {
    modes.iter().copied().map(Mode::new).collect()
}

fn get_extent(modes: &[i32], extent: u64) -> Vec<u64> {
    vec![extent; modes.len()]
}

fn build_distributed_modes(modes: &[i32], value: &str) -> Vec<i32> {
    let cleaned = value.chars().filter(|&c| c != ',').collect::<String>();
    modes
        .iter()
        .copied()
        .filter(|&mode| {
            !cleaned.is_empty() && cleaned.contains(char::from_u32(mode as u32).unwrap_or('\0'))
        })
        .collect()
}

fn get_ranks_per_mode(modes: &[i32], distributed_modes: &[i32], extents: &[u64]) -> Vec<u64> {
    modes
        .iter()
        .zip(extents)
        .map(|(&mode, &extent)| {
            if distributed_modes.contains(&mode) {
                extent
            } else {
                1
            }
        })
        .collect()
}

fn calc_local_extents(global: &[u64], ranks_per_mode: &[u64]) -> Vec<u64> {
    global
        .iter()
        .zip(ranks_per_mode)
        .map(|(&global, &ranks)| ceil_div(global, ranks.max(1)))
        .collect()
}

fn ceil_div(lhs: u64, rhs: u64) -> u64 {
    lhs.div_ceil(rhs)
}

fn product_usize(values: &[u64]) -> Result<usize, Box<dyn std::error::Error>> {
    values.iter().try_fold(1_usize, |product, &value| {
        let value = usize::try_from(value)?;
        product
            .checked_mul(value)
            .ok_or_else(|| "element count overflow".into())
    })
}

fn print_modes(name: &str, values: &[char]) {
    println!(
        "{name}: {}",
        values
            .iter()
            .map(char::to_string)
            .collect::<Vec<_>>()
            .join(" ")
    );
}

fn print_values<T: std::fmt::Display>(name: &str, values: &[T]) {
    println!(
        "{name}: {}",
        values
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<_>>()
            .join(" ")
    );
}

fn print_performance_metrics(
    modes_a: &[i32],
    modes_b: &[i32],
    modes_c: &[i32],
    extent_a: &[u64],
    extent_b: &[u64],
    extent_c: &[u64],
    min_ms: f64,
) {
    let num_k_modes = (modes_a.len() + modes_b.len() - modes_c.len()) / 2;
    let total_a = product_usize(extent_a).unwrap_or(0);
    let total_b = product_usize(extent_b).unwrap_or(0);
    let total_c = product_usize(extent_c).unwrap_or(0);
    let bytes = (total_a + total_b + total_c) * size_of::<Complex32>();
    let gb_per_rank = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
    let gflops_per_rank = (2_usize.pow(num_k_modes as u32) as f64 * total_c as f64 * 8.0)
        / (1024.0 * 1024.0 * 1024.0);
    let seconds = min_ms / 1000.0;
    println!(
        "Performance Summary: min_time: {min_ms:.3} ms, data size: ~{gb_per_rank:.3} GB/rank, \
         gflops: {gflops_per_rank:.3} GFLOPS/rank, bandwidth: ~{:.3} GB/rank/s, \
         gflops: ~{:.3} GFLOPS/rank/s",
        gb_per_rank / seconds,
        gflops_per_rank / seconds
    );
}

fn env_parse<T>(name: &str) -> Option<T>
where
    T: std::str::FromStr,
{
    env::var(name).ok()?.parse().ok()
}
