#![cfg(feature = "live-grid")]

use libremetaverse_types::{
    Vector3,
    compat::{CancellationToken, CancellationTokenSource},
};
use metacrate_grid_agent::{
    EndpointUrl, GridConnection, GridSession, GridSessionBackend, LibremetaverseClientOwner,
    LibremetaverseSceneSource, SceneEntityKind, SceneSnapshot, SceneSource, SecretString,
    SessionSignal, VisionFuture, VisionLimits, VisionService,
};
use std::{
    collections::BTreeMap,
    error::Error,
    path::PathBuf,
    sync::Arc,
    time::{Duration, Instant},
};

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "logs the primary agent into the configured live varregion"]
async fn live_primary_login_resolves_varregion_dimensions() -> Result<(), Box<dyn Error>> {
    let environment = live_environment()?;
    let owner = LibremetaverseClientOwner::new()?;
    let (mut session, cancellation) =
        login(&owner, &environment, "GRID_USER", "GRID_PASSWORD").await?;
    let result = async {
        wait_ready("primary", &mut session, &cancellation).await?;
        let simulator = owner
            .client()
            .network()
            .current_sim()
            .ok_or("primary simulator unavailable")?;
        let size = simulator.region_size();
        println!(
            "LIVE_PRIMARY_REGION=region:{:?},handle:{},size:{}x{},position:{:?}",
            simulator.name,
            simulator.handle,
            size.0,
            size.1,
            owner.agent().sim_position()
        );
        if size != (1024, 1024) {
            return Err(format!("expected 1024x1024 varregion, got {}x{}", size.0, size.1).into());
        }
        Ok(())
    }
    .await;
    let _ = session.logout(cancellation.token()).await;
    result
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "profiles primary login, renderer initialization, and first visible-scene warmup"]
async fn live_primary_startup_profile() -> Result<(), Box<dyn Error>> {
    let total_started = Instant::now();
    let environment = live_environment()?;
    let owner_started = Instant::now();
    let owner = LibremetaverseClientOwner::new()?;
    let owner_initialization = owner_started.elapsed();
    let gpu_task = tokio::task::spawn_blocking(|| {
        let started = Instant::now();
        (
            metacrate_rendering_wgpu::Renderer::new_blocking(),
            started.elapsed(),
        )
    });
    let login_started = Instant::now();
    let (mut session, cancellation) =
        login(&owner, &environment, "GRID_USER", "GRID_PASSWORD").await?;
    let login = login_started.elapsed();
    let ready_started = Instant::now();
    wait_ready("primary", &mut session, &cancellation).await?;
    let readiness = ready_started.elapsed();
    let settle_started = Instant::now();
    wait_scene_settled(&owner).await?;
    let scene_settle = settle_started.elapsed();
    let limits = VisionLimits {
        minimum_interval: Duration::ZERO,
        ..VisionLimits::default()
    };
    let source = LibremetaverseSceneSource::new(&owner, limits);
    let warmup_started = Instant::now();
    let scene = source.capture_scene(0, cancellation.token()).await?;
    let scene_warmup = warmup_started.elapsed();
    let (renderer, gpu_initialization) = gpu_task.await?;
    renderer?;
    println!(
        "LIVE_STARTUP_PROFILE=owner_initialization_ms:{},grid_login_ms:{},grid_readiness_ms:{},scene_settle_ms:{},gpu_initialization_parallel_ms:{},first_scene_warmup_ms:{},first_scene:{:?},total_ms:{}",
        owner_initialization.as_millis(),
        login.as_millis(),
        readiness.as_millis(),
        scene_settle.as_millis(),
        gpu_initialization.as_millis(),
        scene_warmup.as_millis(),
        scene.timings,
        total_started.elapsed().as_millis(),
    );
    let _ = session.logout(cancellation.token()).await;
    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "records the primary region's asynchronous map-name reply"]
async fn live_primary_map_name_reports_varregion_dimensions() -> Result<(), Box<dyn Error>> {
    let environment = live_environment()?;
    let owner = LibremetaverseClientOwner::new()?;
    let (mut session, cancellation) =
        login(&owner, &environment, "GRID_USER", "GRID_PASSWORD").await?;
    let result = async {
        wait_ready("primary", &mut session, &cancellation).await?;
        let simulator = owner
            .client()
            .network()
            .current_sim()
            .ok_or("primary simulator unavailable")?;
        let name = simulator.name.clone();
        let handle = simulator.handle;
        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
        let _subscription = owner
            .client()
            .grid()
            .subscribe_grid_region(Arc::new(move |event| {
                let _ = sender.send(event.region());
            }));
        let started = Instant::now();
        owner
            .client()
            .grid()
            .request_map_region(name.clone(), libremetaverse::GridLayerType::Objects)?;
        let deadline = started + Duration::from_secs(15);
        while Instant::now() < deadline {
            let remaining = deadline.saturating_duration_since(Instant::now());
            let Ok(Some(region)) = tokio::time::timeout(remaining, receiver.recv()).await else {
                break;
            };
            println!(
                "LIVE_MAP_NAME_EVENT=elapsed_ms:{},name:{:?},handle:{},size:{}x{}",
                started.elapsed().as_millis(),
                region.name,
                region.region_handle,
                region.size_x,
                region.size_y
            );
            if region.region_handle == handle && (region.size_x, region.size_y) == (1024, 1024) {
                return Ok::<(), Box<dyn Error>>(());
            }
        }
        Err(format!("no explicit 1024x1024 map-name reply for {name:?}").into())
    }
    .await;
    let _ = session.logout(cancellation.token()).await;
    result
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "logs the primary agent into the configured live grid and writes a JPEG"]
#[allow(clippy::too_many_lines)] // The one end-to-end live workflow stays readable in execution order.
async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Error>> {
    let environment = live_environment()?;
    let renderer_owner = LibremetaverseClientOwner::new()?;
    let limits = VisionLimits {
        minimum_interval: Duration::ZERO,
        ..VisionLimits::default()
    };
    let (_world_events, world_receiver) =
        metacrate_game_loop::LibreMetaverseWorldEvents::subscribe_visible(
            renderer_owner.client(),
            renderer_owner.agent(),
            limits.max_distance_meters,
            4_096,
        )
        .ok_or("live world event subscription failed")?;
    let world_loop = metacrate_game_loop::start(
        metacrate_game_loop::LoopConfig::default(),
        world_receiver,
        (),
        None,
    )
    .ok_or("live world loop failed to start")?;
    let (mut renderer_session, renderer_cancel) =
        login(&renderer_owner, &environment, "GRID_USER", "GRID_PASSWORD").await?;
    let scene_source = Arc::new(LibremetaverseSceneSource::new(&renderer_owner, limits));
    let prefetch_started = Instant::now();
    scene_source.start_prefetch();

    let result = async {
        wait_ready("renderer", &mut renderer_session, &renderer_cancel).await?;
        let renderer_simulator = renderer_owner
            .client()
            .network()
            .current_sim()
            .ok_or("renderer simulator unavailable")?;
        let settled_position = renderer_owner.agent().sim_position();
        let region_size = renderer_simulator.region_size();
        println!(
            "LIVE_RENDER_LOCATION=region:{:?},size:{}x{},position:{settled_position:?}",
            renderer_simulator.name, region_size.0, region_size.1
        );
        wait_scene_settled(&renderer_owner).await?;
        tokio::time::timeout(Duration::from_secs(10), async {
            while world_loop
                .latest_world()
                .is_none_or(|world| world.value.region.is_none())
            {
                tokio::time::sleep(Duration::from_millis(20)).await;
            }
        })
        .await
        .map_err(|_| "central world state did not become ready")?;
        let world = world_loop
            .latest_world()
            .ok_or("central world snapshot disappeared")?;
        println!(
            "LIVE_WORLD_STATE=sequence:{},primitives:{},avatars:{},terrain_patches:{},dropped_events:{}",
            world.sequence,
            world.value.primitives.len(),
            world.value.avatars.len(),
            world.value.terrain.len(),
            world_loop.dropped_world_events(),
        );
        let view_forward = Vector3::mul_with_vector3_quaternion(
            Vector3::unit_x(),
            renderer_owner.agent().sim_rotation(),
        );
        let (camera_position, camera_target) =
            avatar_forward_camera(settled_position, view_forward)?;
        renderer_owner
            .agent()
            .movement
            .camera
            .look_at_with_vector3_vector3(camera_position, camera_target)?;
        renderer_owner
            .agent()
            .movement
            .camera
            .set_vertical_fov_angle(65_f32.to_radians())?;

        tokio::time::timeout(Duration::from_mins(10), async {
            while scene_source.prefetch_passes() == 0 {
                tokio::time::sleep(Duration::from_millis(250)).await;
            }
        })
        .await
        .map_err(|_| "background scene prefetch did not complete")?;
        println!(
            "LIVE_RENDER_PREFETCH_MILLISECONDS={}",
            prefetch_started.elapsed().as_millis()
        );
        scene_source.stop_prefetch().await;

        println!(
            "LIVE_RENDER_SCOPE=max_distance_meters:{}",
            limits.max_distance_meters
        );
        let vision = VisionService::new(
            Arc::new(DiagnosticSceneSource {
                inner: Arc::clone(&scene_source),
                material_override: std::env::var("METACRATE_LIVE_RENDER_MATERIAL_OVERRIDE").ok(),
            }),
            limits,
        )?
        .prefer_gpu();
        let initialization_started = Instant::now();
        if !vision.initialize_renderer().await {
            return Err("live wgpu renderer initialization failed".into());
        }
        println!(
            "LIVE_RENDER_INITIALIZATION_MILLISECONDS={}",
            initialization_started.elapsed().as_millis()
        );
        vision.set_generation(1);
        let mut capture = None;
        let output = std::env::var_os("METACRATE_LIVE_RENDER_OUTPUT").map_or_else(
            || std::env::temp_dir().join("metacrate-live-render.jpg"),
            PathBuf::from,
        );
        let frames = std::env::var("METACRATE_LIVE_RENDER_FRAMES")
            .ok()
            .and_then(|value| value.parse::<u32>().ok())
            .unwrap_or(12)
            .clamp(1, 24);
        let mut previous_missing = u32::MAX;
        let mut stable_missing = 0_u32;
        for frame in 1..=frames {
            let frame_started = Instant::now();
            let next = vision
                .capture(
                    &format!("live-renderer-evidence-{frame}"),
                    1,
                    renderer_cancel.token(),
                )
                .await?;
            println!(
                "LIVE_RENDER_FRAME_{frame}_MILLISECONDS={}",
                frame_started.elapsed().as_millis()
            );
            println!("LIVE_RENDER_FRAME_{frame}_SUMMARY={}", next.summary);
            std::fs::write(&output, &next.jpeg)?;
            if next.completeness.textures_missing == previous_missing {
                stable_missing += 1;
            } else {
                previous_missing = next.completeness.textures_missing;
                stable_missing = 0;
            }
            capture = Some(next);
            if previous_missing == 0 || (frame >= 8 && stable_missing >= 3) {
                break;
            }
        }
        let capture = capture.ok_or("live renderer produced no frame")?;
        let viewport_before = vision
            .viewport_stats()
            .ok_or("viewport loop did not expose statistics")?;
        let soak_started = Instant::now();
        tokio::time::sleep(Duration::from_secs(2)).await;
        let soak_elapsed = soak_started.elapsed();
        let viewport_after = vision
            .viewport_stats()
            .ok_or("viewport loop stopped exposing statistics")?;
        let soak_frames = viewport_after
            .completed_frames
            .saturating_sub(viewport_before.completed_frames);
        let soak_missed = viewport_after
            .missed_frame_deadlines
            .saturating_sub(viewport_before.missed_frame_deadlines);
        let soak_fps = f64::from(u32::try_from(soak_frames)?) / soak_elapsed.as_secs_f64();
        println!(
            "LIVE_VIEWPORT_SOAK=frames:{soak_frames},elapsed_ms:{},fps:{soak_fps:.2},missed_deadlines:{soak_missed},render_errors:{},last_render_ms:{}",
            soak_elapsed.as_millis(),
            viewport_after.render_errors,
            viewport_after.last_render.as_millis(),
        );
        if soak_fps < 10.0 || soak_missed != 0 || viewport_after.render_errors != 0 {
            return Err("stable viewport did not sustain its 10 Hz target".into());
        }
        profile_warm_capture(&scene_source, limits, renderer_cancel.token()).await?;
        println!("LIVE_RENDER_OUTPUT={}", output.display());
        println!("LIVE_RENDER_SHA256={}", capture.image_sha256);
        Ok::<(), Box<dyn Error>>(())
    }
    .await;

    scene_source.stop_prefetch().await;
    world_loop
        .shutdown()
        .map_err(|_| "world loop thread panicked")?;
    let _ = renderer_session.logout(renderer_cancel.token()).await;
    result
}

#[allow(clippy::too_many_lines)] // One ordered live timing record keeps phase boundaries explicit.
async fn profile_warm_capture(
    source: &LibremetaverseSceneSource,
    limits: VisionLimits,
    cancellation: CancellationToken,
) -> Result<(), Box<dyn Error>> {
    let total_started = Instant::now();
    let scene_started = Instant::now();
    let scene = source.capture_scene(1, cancellation).await?;
    let scene_total = scene_started.elapsed();
    let preparation_started = Instant::now();
    let mut renderables = scene
        .entities
        .iter()
        .flat_map(|entity| {
            entity
                .renderables
                .iter()
                .cloned()
                .map(move |renderable| (entity.id.to_string(), renderable))
        })
        .collect::<Vec<_>>();
    renderables.sort_by(|left, right| left.0.cmp(&right.0));
    let renderables = renderables
        .into_iter()
        .map(|(_, renderable)| renderable)
        .collect::<Vec<_>>();
    let renderables: Arc<[metacrate_rendering_wgpu::Renderable]> = renderables.into();
    let preparation = preparation_started.elapsed();
    let renderer_started = Instant::now();
    let renderer = metacrate_game_loop::ViewportRuntime::new_blocking()?;
    let renderer_initialization = renderer_started.elapsed();
    let render_limits = metacrate_rendering_wgpu::RenderLimits {
        width: limits.width,
        height: limits.height,
        max_triangles: limits.max_triangles,
        max_texture_bytes: limits.max_texture_bytes,
        max_texture_pixels: limits.max_decode_pixels,
        far_distance: 512,
    };
    let first = renderer.render_scene(
        scene.scene_sequence,
        scene.camera,
        || Arc::clone(&renderables),
        Arc::clone(&scene.textures),
        [125, 149, 173, 255],
        render_limits,
    )?;
    let refresh_started = Instant::now();
    let refresh = renderer.render_scene(
        scene.scene_sequence.saturating_add(1),
        scene.camera,
        || Arc::clone(&renderables),
        Arc::clone(&scene.textures),
        [125, 149, 173, 255],
        render_limits,
    )?;
    let refresh_total = refresh_started.elapsed();
    let warm_started = Instant::now();
    let warm = renderer.render_scene(
        scene.scene_sequence.saturating_add(1),
        scene.camera,
        || panic!("unchanged scene was transferred to the renderer again"),
        Arc::clone(&scene.textures),
        [125, 149, 173, 255],
        render_limits,
    )?;
    let warm_render_total = warm_started.elapsed();
    let jpeg_started = Instant::now();
    let rgb = warm
        .frame
        .value
        .rgba
        .chunks_exact(4)
        .flat_map(|pixel| pixel[..3].iter().copied())
        .collect::<Vec<_>>();
    let mut jpeg = Vec::new();
    jpeg_encoder::Encoder::new(&mut jpeg, 92).encode(
        &rgb,
        u16::try_from(limits.width)?,
        u16::try_from(limits.height)?,
        jpeg_encoder::ColorType::Rgb,
    )?;
    let jpeg_encode = jpeg_started.elapsed();
    println!(
        "LIVE_RENDER_PROFILE=scene_total_ms:{},terrain_wait_ms:{},object_snapshot_and_asset_discovery_ms:{},material_fetch_ms:{},asset_fetch_ms:{},decode_and_geometry_ms:{},renderable_collection_and_sort_ms:{},renderer_initialization_ms:{},first_render_total_ms:{},first_render:{:?},scene_refresh_total_ms:{},scene_refresh:{:?},warm_render_total_ms:{},warm_render:{:?},jpeg_encode_ms:{},profile_total_ms:{}",
        scene_total.as_millis(),
        scene.timings.terrain_wait.as_millis(),
        scene
            .timings
            .object_snapshot_and_asset_discovery
            .as_millis(),
        scene.timings.material_fetch.as_millis(),
        scene.timings.asset_fetch.as_millis(),
        scene.timings.decode_and_geometry.as_millis(),
        preparation.as_millis(),
        renderer_initialization.as_millis(),
        render_total(&first.timings).as_millis(),
        first.timings,
        refresh_total.as_millis(),
        refresh.timings,
        warm_render_total.as_millis(),
        warm.timings,
        jpeg_encode.as_millis(),
        total_started.elapsed().as_millis(),
    );
    if refresh_total >= metacrate_game_loop::DEFAULT_VIEWPORT_FRAME_INTERVAL {
        return Err("cached scene refresh exceeded the viewport frame interval".into());
    }
    Ok(())
}

fn render_total(timings: &metacrate_rendering_wgpu::RenderTimings) -> Duration {
    timings.validation
        + timings.command_copy
        + timings.clear_previous_frame
        + timings.texture_cache_sync
        + timings.mesh_cache_sync
        + timings.scene_setup
        + timings.capture
}

struct DiagnosticSceneSource {
    inner: Arc<LibremetaverseSceneSource>,
    material_override: Option<String>,
}

impl SceneSource for DiagnosticSceneSource {
    fn capture_scene(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> VisionFuture<'_, SceneSnapshot> {
        Box::pin(async move {
            let mut scene = self.inner.capture_scene(generation, cancellation).await?;
            if let Some(material_override) = &self.material_override {
                for renderable in Arc::make_mut(&mut scene.entities)
                    .iter_mut()
                    .filter(|entity| entity.kind == SceneEntityKind::Object)
                    .flat_map(|entity| &mut entity.renderables)
                {
                    match &mut renderable.material {
                        metacrate_rendering_wgpu::Material::BlinnPhong(material) => {
                            if material_override == "solid" {
                                *material = metacrate_rendering_wgpu::BlinnPhongMaterial {
                                    diffuse_color_srgb: [255, 0, 255, 255],
                                    fullbright: true,
                                    double_sided: true,
                                    ..Default::default()
                                };
                            } else if material_override == "unlit-white" {
                                material.diffuse_color_srgb = [255; 4];
                                material.fullbright = true;
                            }
                        }
                        metacrate_rendering_wgpu::Material::Pbr(material) => {
                            if material_override == "solid" {
                                *material = metacrate_rendering_wgpu::PbrMaterial {
                                    base_color_srgb: [0, 255, 255, 255],
                                    emissive_factor_srgb: [0, 255, 255],
                                    metallic_factor: 0.0,
                                    roughness_factor: 1.0,
                                    double_sided: true,
                                    ..Default::default()
                                };
                            }
                        }
                    }
                }
            }
            Ok(scene)
        })
    }
}

async fn login(
    owner: &LibremetaverseClientOwner,
    environment: &BTreeMap<String, String>,
    user: &'static str,
    password: &'static str,
) -> Result<(Box<dyn GridSession>, CancellationTokenSource), Box<dyn Error>> {
    let cancellation = CancellationTokenSource::new();
    let backend = owner.session_backend(GridConnection {
        login_url: EndpointUrl::parse("grid.login_url", &required(environment, "GRID_LOGIN_URL")?)?,
        avatar_name: required(environment, user)?,
        password: SecretString::new("grid.password", required(environment, password)?)?,
    })?;
    let session = backend.login(1, cancellation.token()).await?;
    Ok((session, cancellation))
}

async fn wait_ready(
    account: &str,
    session: &mut Box<dyn GridSession>,
    cancellation: &CancellationTokenSource,
) -> Result<(), Box<dyn Error>> {
    let signal = tokio::time::timeout(
        Duration::from_secs(30),
        session.next_signal(cancellation.token()),
    )
    .await
    .map_err(|_| format!("timed out waiting for {account} session readiness"))?;
    match signal {
        SessionSignal::Ready => Ok(()),
        signal => Err(format!("unexpected {account} session signal: {signal:?}").into()),
    }
}

async fn wait_scene_settled(owner: &LibremetaverseClientOwner) -> Result<(), Box<dyn Error>> {
    let deadline = Instant::now() + Duration::from_secs(30);
    let minimum = Instant::now() + Duration::from_secs(10);
    let mut previous = 0;
    let mut stable = 0;
    loop {
        tokio::time::sleep(Duration::from_secs(1)).await;
        let simulator = owner
            .client()
            .network()
            .current_sim()
            .ok_or("simulator unavailable while settling scene")?;
        let count = simulator
            .objects_primitives
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .len();
        if count == previous && count != 0 {
            stable += 1;
        } else {
            stable = 0;
            previous = count;
        }
        if (Instant::now() >= minimum && stable >= 3) || Instant::now() >= deadline {
            return Ok(());
        }
    }
}

fn live_environment() -> Result<BTreeMap<String, String>, Box<dyn Error>> {
    let mut values = std::env::vars().collect::<BTreeMap<_, _>>();
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.env");
    for line in std::fs::read_to_string(path)?.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        values.entry(key.trim().to_owned()).or_insert_with(|| {
            value
                .trim()
                .trim_matches(|character| character == '\'' || character == '"')
                .to_owned()
        });
    }
    Ok(values)
}

fn required(
    environment: &BTreeMap<String, String>,
    key: &'static str,
) -> Result<String, Box<dyn Error>> {
    environment
        .get(key)
        .filter(|value| !value.is_empty())
        .cloned()
        .ok_or_else(|| format!("missing live test setting {key}").into())
}

#[allow(
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::cast_sign_loss
)] // Coordinates are already clamped inside the non-negative simulator extent.
fn avatar_forward_camera(
    avatar: Vector3,
    forward: Vector3,
) -> Result<(Vector3, Vector3), Box<dyn Error>> {
    let length = (forward.x * forward.x + forward.y * forward.y + forward.z * forward.z).sqrt();
    if !length.is_finite() || length < f32::EPSILON {
        return Err("avatar facing direction unavailable".into());
    }
    let position = Vector3 {
        x: avatar.x,
        y: avatar.y,
        z: avatar.z + 1.6,
    };
    Ok((
        position,
        Vector3 {
            x: position.x + forward.x / length * 24.0,
            y: position.y + forward.y / length * 24.0,
            z: position.z + forward.z / length * 24.0,
        },
    ))
}

#[test]
fn evidence_camera_uses_avatar_eye_position_and_facing() {
    let avatar = Vector3 {
        x: 10.0,
        y: 20.0,
        z: 30.0,
    };
    let (position, target) = avatar_forward_camera(avatar, Vector3::unit_y()).unwrap();
    assert_eq!(
        position,
        Vector3 {
            x: 10.0,
            y: 20.0,
            z: 31.6
        }
    );
    assert_eq!(
        target,
        Vector3 {
            x: 10.0,
            y: 44.0,
            z: 31.6
        }
    );
}
