use std::{fs, time::Instant};

use lumen_engine::composition::Composition;
use lumen_ffmpeg::{
    AudioEncoderConfig, CudaDriver, EncodeMode, GpuBackend, GpuVideoInput, MuxedEncoder,
    VideoCodec, VideoEncoderConfig, import_owned_vulkan_opaque_fd_image,
};

use crate::render::{
    RenderError, RenderProgress,
    encoder::has_audio,
    frame_timing::{FRAME_TIMING_LOG_INTERVAL, FrameTiming, FrameTimingTotals},
    media::LocalMediaStore,
};

use super::{MEDIA_PREFETCH_LOOKAHEAD_FRAMES, create_gpu_renderer, prefetch_media_frames};

#[allow(clippy::too_many_arguments)]
pub(super) fn render_project_mp4_cuda(
    composition: &Composition,
    media_store: &LocalMediaStore,
    width: u32,
    height: u32,
    fps: f32,
    total_frames: u32,
    codec: VideoCodec,
    verbose_debug: bool,
    on_progress: &mut dyn FnMut(RenderProgress),
) -> Result<Vec<u8>, RenderError> {
    let tmp = tempfile::tempdir().map_err(|err| RenderError {
        code: "encode_failed",
        message: format!("failed to create temp dir: {err}"),
        retryable: true,
    })?;
    let output_path = tmp.path().join("output.mp4");
    let mut renderer = create_gpu_renderer().map_err(|err| RenderError {
        code: "render_failed",
        message: format!("failed to create GPU renderer: {err:#}"),
        retryable: true,
    })?;
    renderer
        .compile_with_media(
            composition,
            media_store,
            lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
        )
        .map_err(|err| RenderError {
            code: "render_failed",
            message: format!("failed to compile composition: {err}"),
            retryable: false,
        })?;

    let size = lumen_gpu::Size::new(width, height);
    let exportable = renderer
        .gpu_renderer()
        .create_exportable_vulkan_texture(
            Some("lumen-server vk-cuda encode texture"),
            size,
            lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
            lumen_gpu::wgpu::TextureUsages::COPY_DST
                | lumen_gpu::wgpu::TextureUsages::COPY_SRC
                | lumen_gpu::wgpu::TextureUsages::TEXTURE_BINDING,
        )
        .map_err(|err| RenderError {
            code: "render_failed",
            message: format!("failed to create exportable Vulkan texture: {err}"),
            retryable: false,
        })?;
    let vulkan_device = exportable.device_info().clone();
    if verbose_debug {
        tracing::debug!(
            width,
            height,
            allocation_size = exportable.allocation_size(),
            row_pitch = exportable.row_pitch(),
            memory_fd = exportable.memory_fd_raw(),
            memory_type_index = exportable.memory_type_index(),
            vulkan_device_name = %vulkan_device.name,
            vulkan_vendor_id = vulkan_device.vendor_id,
            vulkan_device_id = vulkan_device.device_id,
            vulkan_device_type = %vulkan_device.device_type,
            vulkan_device_uuid = %format_uuid(&vulkan_device.device_uuid),
            vulkan_driver_uuid = %format_uuid(&vulkan_device.driver_uuid),
            "created exportable Vulkan texture for CUDA import"
        );
    }
    let driver = CudaDriver::load().map_err(|err| RenderError {
        code: "encode_failed",
        message: format!("failed to load CUDA driver: {err}"),
        retryable: false,
    })?;
    match driver.driver_version() {
        Ok(version) if verbose_debug => {
            tracing::debug!(cuda_driver_version = version, "loaded CUDA driver");
        }
        Ok(_) => {}
        Err(err) => tracing::warn!(error = %err, "failed to query CUDA driver version"),
    }
    let cuda_devices = driver.devices().map_err(|err| RenderError {
        code: "encode_failed",
        message: format!("failed to enumerate CUDA devices: {err}"),
        retryable: false,
    })?;
    for device in &cuda_devices {
        if verbose_debug {
            tracing::debug!(
                cuda_ordinal = device.ordinal,
                cuda_name = %device.name,
                cuda_uuid = %format_uuid(&device.uuid),
                cuda_pci_bus_id = %device.pci_bus_id,
                matches_vulkan_device = device.uuid == vulkan_device.device_uuid,
                "found CUDA device"
            );
        }
    }
    let cuda_ordinal = cuda_devices
        .iter()
        .find(|device| device.uuid == vulkan_device.device_uuid)
        .map(|device| device.ordinal)
        .ok_or_else(|| RenderError {
            code: "encode_failed",
            message: format!(
                "no CUDA device UUID matched Vulkan device {} ({})",
                vulkan_device.name,
                format_uuid(&vulkan_device.device_uuid)
            ),
            retryable: false,
        })?;
    let context = driver
        .create_primary_context_for_ordinal(cuda_ordinal)
        .map_err(|err| RenderError {
            code: "encode_failed",
            message: format!(
                "failed to create CUDA primary context for device {cuda_ordinal}: {err}"
            ),
            retryable: false,
        })?;
    tracing::info!(
        cuda_ordinal = context.ordinal(),
        vulkan_device_uuid = %format_uuid(&vulkan_device.device_uuid),
        "created CUDA primary context for Vulkan device"
    );
    let imported = import_owned_vulkan_opaque_fd_image(
        &driver,
        exportable
            .memory_fd()
            .try_clone()
            .map_err(|err| RenderError {
                code: "encode_failed",
                message: format!("failed to duplicate Vulkan memory fd: {err}"),
                retryable: false,
            })?,
        exportable.allocation_size(),
        width,
        height,
    )
    .map_err(|err| RenderError {
        code: "encode_failed",
        message: format!(
            "failed to import Vulkan image into CUDA on device {cuda_ordinal}; vulkan_device={} vulkan_uuid={} allocation_size={} row_pitch={} fd={}: {err}",
            vulkan_device.name,
            format_uuid(&vulkan_device.device_uuid),
            exportable.allocation_size(),
            exportable.row_pitch(),
            exportable.memory_fd_raw()
        ),
        retryable: false,
    })?;
    let cuda_frame = driver
        .allocate_rgba_frame(width, height)
        .map_err(|err| RenderError {
            code: "encode_failed",
            message: format!("failed to allocate CUDA RGBA frame: {err}"),
            retryable: false,
        })?;

    let include_audio = has_audio(composition);
    let mut config =
        VideoEncoderConfig::cpu_rgba(width, height, fps.round().max(1.0) as u32, codec);
    config.mode = EncodeMode::GpuTexture(GpuBackend::Cuda);
    config.bit_rate = 14_000_000;
    let audio = include_audio.then(|| {
        AudioEncoderConfig::aac(
            lumen_engine::audio::AUDIO_SAMPLE_RATE,
            lumen_engine::audio::AUDIO_CHANNELS as u16,
        )
    });
    let mut encoder =
        MuxedEncoder::create_with_audio(output_path.to_string_lossy().to_string(), config, audio)
            .map_err(|err| RenderError {
            code: "encode_failed",
            message: err.to_string(),
            retryable: true,
        })?;

    tracing::info!(
        width,
        height,
        total_frames,
        "using Vulkan-to-CUDA NVENC render path"
    );
    let mut timing_totals = FrameTimingTotals::default();
    for frame in 0..total_frames {
        let mut timing = FrameTiming::default();
        let started = Instant::now();
        prefetch_media_frames(composition, media_store, frame);
        timing.prefetch = started.elapsed();

        let started = Instant::now();
        let (raster, _) = renderer
            .render_frame_submitted(composition, frame, media_store)
            .map_err(|err| RenderError {
                code: "render_failed",
                message: format!("render failed at frame {frame}: {err}"),
                retryable: true,
            })?;
        timing.render_submit = started.elapsed();

        let started = Instant::now();
        let _ = renderer.precompile_frame_window(
            composition,
            frame.saturating_add(1),
            MEDIA_PREFETCH_LOOKAHEAD_FRAMES,
            media_store,
        );
        timing.precompile = started.elapsed();

        let started = Instant::now();
        renderer
            .gpu_renderer()
            .copy_texture_to_external(raster.texture, exportable.texture())
            .map_err(|err| RenderError {
                code: "render_failed",
                message: format!("failed to copy frame {frame} into exportable texture: {err}"),
                retryable: true,
            })?;
        renderer
            .gpu_renderer()
            .device
            .poll(lumen_gpu::wgpu::PollType::wait_indefinitely())
            .map_err(|err| RenderError {
                code: "render_failed",
                message: format!("GPU poll failed at frame {frame}: {err}"),
                retryable: true,
            })?;
        timing.gpu_wait = started.elapsed();

        let started = Instant::now();
        context.set_current().map_err(|err| RenderError {
            code: "encode_failed",
            message: format!("failed to restore CUDA context: {err}"),
            retryable: true,
        })?;
        driver
            .copy_image_to_rgba_frame(&imported, &cuda_frame)
            .map_err(|err| RenderError {
                code: "encode_failed",
                message: format!("failed to copy Vulkan frame into CUDA frame: {err}"),
                retryable: true,
            })?;
        timing.cuda_copy = started.elapsed();

        let started = Instant::now();
        let frame_ref = cuda_frame.as_video_frame(Some(i64::from(frame)));
        encoder
            .write_gpu_frame(&GpuVideoInput::Cuda(&frame_ref))
            .map_err(|err| RenderError {
                code: "encode_failed",
                message: err.to_string(),
                retryable: true,
            })?;
        timing.encode_write = started.elapsed();

        let completed = frame.saturating_add(1);
        let ratio = (completed as f32 / total_frames as f32).clamp(0.0, 1.0);
        let started = Instant::now();
        on_progress(RenderProgress {
            stage: "rendering",
            frame: completed,
            total_frames,
            ratio,
        });
        timing.progress = started.elapsed();
        timing_totals.add(timing);
        if verbose_debug
            && (completed % FRAME_TIMING_LOG_INTERVAL == 0 || completed == total_frames)
        {
            timing_totals.log(completed, total_frames, "cuda_nvenc");
        }
    }

    let finish_started = Instant::now();
    if include_audio {
        let audio_started = Instant::now();
        crate::render::encoder::write_composited_audio_with(composition, media_store, |frame| {
            encoder
                .write_audio_frame(&frame)
                .map_err(|err| anyhow::anyhow!(err.to_string()))
        })
        .map_err(|err| RenderError {
            code: "audio_render_failed",
            message: err.to_string(),
            retryable: true,
        })?;
        if verbose_debug {
            tracing::debug!(
                audio_ms = audio_started.elapsed().as_millis(),
                "finished composited audio write"
            );
        }
    }

    let encoder_finish_started = Instant::now();
    encoder.finish().map_err(|err| RenderError {
        code: "encode_failed",
        message: err.to_string(),
        retryable: true,
    })?;
    let encoder_finish_ms = encoder_finish_started.elapsed().as_millis();

    let read_started = Instant::now();
    fs::read(&output_path)
        .map_err(|err| RenderError {
            code: "encode_failed",
            message: format!("failed to read encoded output: {err}"),
            retryable: true,
        })
        .inspect(|bytes| {
            if verbose_debug {
                tracing::debug!(
                    encoder_finish_ms,
                    read_output_ms = read_started.elapsed().as_millis(),
                    finalization_ms = finish_started.elapsed().as_millis(),
                    output_bytes = bytes.len(),
                    "finished CUDA/NVENC render finalization"
                );
            }
        })
}

fn format_uuid(uuid: &[u8; 16]) -> String {
    uuid.iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<Vec<_>>()
        .join("")
}
