//! Functions for handling and converting IDs.

use anyhow::Result;
use kcmc::ModelingCmd;
use kcmc::each_cmd as mcmd;
use kittycad_modeling_cmds::ok_response::OkModelingCmdResponse;
use kittycad_modeling_cmds::shared::Point3d;
use kittycad_modeling_cmds::websocket::OkWebSocketResponseData;
use kittycad_modeling_cmds::{self as kcmc};

use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::exec::KclValue;
use crate::execution::ExecState;
use crate::execution::ExtrudeSurface;
use crate::execution::GeoMeta;
use crate::execution::Geometry;
use crate::execution::Metadata;
use crate::execution::ModelingCmdMeta;
use crate::execution::Solid;
use crate::execution::TagEngineInfo;
use crate::execution::TagIdentifier;
use crate::execution::types::RuntimeType;
use crate::parsing::ast::types::TagDeclarator;
use crate::std::Args;
use crate::std::args::TyF64;

/// Translates face indices to face IDs.
pub async fn face_id(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
    let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
    let face_index: u32 = args.get_kw_arg("index", &RuntimeType::count(), exec_state)?;

    inner_face_id(body, face_index, exec_state, args).await
}

/// Translates face indices to face IDs.
async fn inner_face_id(
    body: Solid,
    face_index: u32,
    exec_state: &mut ExecState,
    args: Args,
) -> Result<KclValue, KclError> {
    let no_engine_commands = args.ctx.no_engine_commands().await;
    // Handle mock execution
    let face_id = if no_engine_commands {
        exec_state.next_uuid()
    } else {
        // Query engine, unpack response.
        let face_uuid_response = exec_state
            .send_modeling_cmd(
                ModelingCmdMeta::from_args(exec_state, &args),
                ModelingCmd::from(
                    mcmd::Solid3dGetFaceUuid::builder()
                        .object_id(body.id)
                        .face_index(face_index)
                        .build(),
                ),
            )
            .await?;
        let OkWebSocketResponseData::Modeling {
            modeling_response: OkModelingCmdResponse::Solid3dGetFaceUuid(inner_resp),
        } = face_uuid_response
        else {
            return Err(KclError::new_semantic(KclErrorDetails::new(
                format!(
                    "Engine returned invalid response, it should have returned Solid3dGetFaceUuid but it returned {face_uuid_response:?}"
                ),
                vec![args.source_range],
            )));
        };
        inner_resp.face_id
    };

    let new_tag_name = format!("face_id_{}", face_id.to_string().replace('-', "_"));
    let new_tag_node = TagDeclarator::new(&new_tag_name);

    let mut tagged_surface = body
        .value
        .iter()
        .find(|surface| surface.face_id() == face_id)
        .cloned()
        .unwrap_or_else(|| {
            // Booleans and imported solids can have engine face IDs that we don't track in
            // `body.value`, but `faceId` should still return a usable tagged face.
            ExtrudeSurface::ExtrudePlane(crate::execution::ExtrudePlane {
                face_id,
                tag: None,
                geo_meta: GeoMeta {
                    id: face_id,
                    metadata: args.source_range.into(),
                },
            })
        });
    tagged_surface.set_surface_tag(&new_tag_node);

    let new_tag = TagIdentifier {
        value: new_tag_name,
        info: vec![(
            exec_state.stack().current_epoch(),
            TagEngineInfo {
                id: tagged_surface.get_id(),
                geometry: Geometry::Solid(body),
                path: None,
                surface: Some(tagged_surface),
            },
        )],
        meta: vec![Metadata {
            source_range: args.source_range,
        }],
    };

    Ok(KclValue::TagIdentifier(Box::new(new_tag)))
}

/// Translates edge indices to edge IDs.
pub async fn edge_id(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
    let body = args.get_unlabeled_kw_arg("body", &RuntimeType::solid(), exec_state)?;
    let edge_index: Option<u32> = args.get_kw_arg_opt("index", &RuntimeType::count(), exec_state)?;
    let closest_to: Option<[TyF64; 3]> = args.get_kw_arg_opt("closestTo", &RuntimeType::point3d(), exec_state)?;
    let closest_to = closest_to
        .map(|point| [point[0].to_mm(), point[1].to_mm(), point[2].to_mm()])
        .map(|[x, y, z]| Point3d { x, y, z });
    match (edge_index, closest_to) {
        (None, None) => Err(KclError::new_semantic(KclErrorDetails::new(
            "Must use either `index` or `closestTo`".to_string(),
            vec![args.source_range],
        ))),
        (None, Some(closest_to)) => inner_edge_id_by_point(body, closest_to, exec_state, args).await,
        (Some(edge_index), None) => inner_edge_id(body, edge_index, exec_state, args).await,
        (Some(_), Some(_)) => Err(KclError::new_semantic(KclErrorDetails::new(
            "Cannot use both `index` and `closestTo`".to_string(),
            vec![args.source_range],
        ))),
    }
}

/// Translates edge indices to edge IDs.
async fn inner_edge_id(
    body: Solid,
    edge_index: u32,
    exec_state: &mut ExecState,
    args: Args,
) -> Result<KclValue, KclError> {
    // Handle mock execution
    let no_engine_commands = args.ctx.no_engine_commands().await;
    let edge_id = if no_engine_commands {
        exec_state.next_uuid()
    } else {
        let edge_uuid_response = exec_state
            .send_modeling_cmd(
                ModelingCmdMeta::from_args(exec_state, &args),
                ModelingCmd::from(
                    mcmd::Solid3dGetEdgeUuid::builder()
                        .object_id(body.id)
                        .edge_index(edge_index)
                        .build(),
                ),
            )
            .await?;

        let OkWebSocketResponseData::Modeling {
            modeling_response: OkModelingCmdResponse::Solid3dGetEdgeUuid(inner_resp),
        } = edge_uuid_response
        else {
            return Err(KclError::new_semantic(KclErrorDetails::new(
                format!(
                    "Engine returned invalid response, it should have returned Solid3dGetEdgeUuid but it returned {edge_uuid_response:?}"
                ),
                vec![args.source_range],
            )));
        };
        inner_resp.edge_id
    };
    Ok(KclValue::Uuid {
        value: edge_id,
        meta: vec![Metadata {
            source_range: args.source_range,
        }],
    })
}

/// Finds ID of edge closest to this point.
async fn inner_edge_id_by_point(
    body: Solid,
    closest_point: Point3d<f64>,
    exec_state: &mut ExecState,
    args: Args,
) -> Result<KclValue, KclError> {
    // Handle mock execution
    let no_engine_commands = args.ctx.no_engine_commands().await;
    let edge_id = if no_engine_commands {
        exec_state.next_uuid()
    } else {
        let edge_uuid_response = exec_state
            .send_modeling_cmd(
                ModelingCmdMeta::from_args(exec_state, &args),
                ModelingCmd::from(
                    mcmd::ClosestEdge::builder()
                        .object_id(body.id)
                        .closest_to(closest_point)
                        .build(),
                ),
            )
            .await?;

        let OkWebSocketResponseData::Modeling {
            modeling_response: OkModelingCmdResponse::ClosestEdge(inner_resp),
        } = edge_uuid_response
        else {
            return Err(KclError::new_semantic(KclErrorDetails::new(
                format!(
                    "Engine returned invalid response, it should have returned ClosestEdge but it returned {edge_uuid_response:?}"
                ),
                vec![args.source_range],
            )));
        };
        let Some(edge_id) = inner_resp.edge_id else {
            return Err(KclError::new_semantic(KclErrorDetails::new(
                "Engine didn't find any edges near this point".to_string(),
                vec![args.source_range],
            )));
        };
        edge_id
    };
    Ok(KclValue::Uuid {
        value: edge_id,
        meta: vec![Metadata {
            source_range: args.source_range,
        }],
    })
}
