// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::api::RpcService;
use crate::api::error::ObjectNotFoundError;
use crate::api::error::RpcError;
use crate::proto::google::rpc::bad_request::FieldViolation;
use crate::proto::soma::BatchGetObjectsRequest;
use crate::proto::soma::BatchGetObjectsResponse;
use crate::proto::soma::ErrorReason;
use crate::proto::soma::GetObjectRequest;
use crate::proto::soma::GetObjectResponse;
use crate::proto::soma::GetObjectResult;
use crate::proto::soma::Object;
use crate::types::Address;
use crate::utils::field::FieldMaskTree;
use crate::utils::field::FieldMaskUtil;
use crate::utils::merge::Merge;
use prost_types::FieldMask;

pub const READ_MASK_DEFAULT: &str = "object_id,version,digest";

type ValidationResult = Result<(Vec<(Address, Option<u64>)>, FieldMaskTree), RpcError>;

pub fn validate_get_object_requests(
    requests: Vec<(Option<String>, Option<u64>)>,
    read_mask: Option<FieldMask>,
) -> ValidationResult {
    let read_mask = {
        let read_mask = read_mask.unwrap_or_else(|| FieldMask::from_str(READ_MASK_DEFAULT));
        read_mask.validate::<Object>().map_err(|path| {
            FieldViolation::new("read_mask")
                .with_description(format!("invalid read_mask path: {path}"))
                .with_reason(ErrorReason::FieldInvalid)
        })?;
        FieldMaskTree::from(read_mask)
    };
    let requests = requests
        .into_iter()
        .enumerate()
        .map(|(idx, (object_id, version))| {
            let object_id = object_id
                .as_ref()
                .ok_or_else(|| {
                    FieldViolation::new("object_id")
                        .with_reason(ErrorReason::FieldMissing)
                        .nested_at("requests", idx)
                })?
                .parse()
                .map_err(|e| {
                    FieldViolation::new("object_id")
                        .with_description(format!("invalid object_id: {e}"))
                        .with_reason(ErrorReason::FieldInvalid)
                        .nested_at("requests", idx)
                })?;
            Ok((object_id, version))
        })
        .collect::<Result<_, RpcError>>()?;
    Ok((requests, read_mask))
}

#[tracing::instrument(skip(service))]
pub fn get_object(
    service: &RpcService,
    GetObjectRequest { object_id, version, read_mask, .. }: GetObjectRequest,
) -> Result<GetObjectResponse, RpcError> {
    let (requests, read_mask) =
        validate_get_object_requests(vec![(object_id, version)], read_mask)?;
    let (object_id, version) = requests[0];
    get_object_impl(service, object_id, version, &read_mask).map(GetObjectResponse::new)
}

#[tracing::instrument(skip(service))]
pub fn batch_get_objects(
    service: &RpcService,
    BatchGetObjectsRequest { requests, read_mask, .. }: BatchGetObjectsRequest,
) -> Result<BatchGetObjectsResponse, RpcError> {
    let requests = requests.into_iter().map(|req| (req.object_id, req.version)).collect();
    let (requests, read_mask) = validate_get_object_requests(requests, read_mask)?;
    let objects = requests
        .into_iter()
        .map(|(object_id, version)| get_object_impl(service, object_id, version, &read_mask))
        .map(|result| match result {
            Ok(object) => GetObjectResult::new_object(object),
            Err(error) => GetObjectResult::new_error(error.into_status_proto()),
        })
        .collect();
    Ok(BatchGetObjectsResponse::new(objects))
}

#[tracing::instrument(skip(service))]
fn get_object_impl(
    service: &RpcService,
    object_id: Address,
    version: Option<u64>,
    read_mask: &FieldMaskTree,
) -> Result<Object, RpcError> {
    let object = if let Some(version) = version {
        service
            .reader
            .inner()
            .get_object_by_key(&object_id.into(), types::object::Version::from_u64(version))
            .ok_or_else(|| ObjectNotFoundError::new_with_version(object_id, version))?
    } else {
        service
            .reader
            .inner()
            .get_object(&object_id.into())
            .ok_or_else(|| ObjectNotFoundError::new(object_id))?
    };

    let mut message = Object::default();
    message.merge(&object, read_mask);

    Ok(message)
}
