//! This module provides bindings to translate programs or fetching Quil-T calibrations
//! from the QCS API.

use std::{collections::HashMap, time::Duration};

use qcs_api_client_grpc::{
    models::controller::EncryptedControllerJob,
    services::translation::{
        self, translate_quil_to_encrypted_controller_job_request::NumShots,
        translation_options::TranslationBackend, TranslateQuilToEncryptedControllerJobRequest,
        TranslationOptions as ApiTranslationOptions,
    },
};
use qcs_api_client_openapi::{
    apis::{translation_api, Error as OpenAPIError},
    models::GetQuiltCalibrationsResponse,
};
use tokio::time::error::Elapsed;

use crate::client::{GrpcClientError, Qcs, DEFAULT_HTTP_API_TIMEOUT};

/// An encrypted and translated program, along with `readout_map`
/// to map job `readout_data` back to program-declared variables.
#[derive(Debug)]
pub struct EncryptedTranslationResult {
    /// The encrypted, translated program.
    pub job: EncryptedControllerJob,

    /// A mapping of translated program variable names,
    /// which will be returned from job execution,
    /// back to the original pre-translation user-defined
    /// program variable names.
    pub readout_map: HashMap<String, String>,
}

/// Translate a program, returning an encrypted and translated program.
pub async fn translate<TO>(
    quantum_processor_id: &str,
    quil_program: &str,
    num_shots: u32,
    client: &Qcs,
    translation_options: Option<TO>,
) -> Result<EncryptedTranslationResult, GrpcClientError>
where
    TO: Into<ApiTranslationOptions>,
{
    #[cfg(feature = "tracing")]
    tracing::debug!(
        %num_shots,
        "translating program for {}",
        quantum_processor_id,
    );

    let options = translation_options.map(Into::into);

    let request = TranslateQuilToEncryptedControllerJobRequest {
        quantum_processor_id: quantum_processor_id.to_owned(),
        num_shots: Some(NumShots::NumShotsValue(num_shots)),
        quil_program: quil_program.to_owned(),
        options,
    };

    let response = client
        .get_translation_client()?
        .translate_quil_to_encrypted_controller_job(request)
        .await?
        .into_inner();

    Ok(EncryptedTranslationResult {
        job: response
            .job
            .ok_or_else(|| GrpcClientError::ResponseEmpty("Encrypted Job".into()))?,
        readout_map: response
            .metadata
            .ok_or_else(|| GrpcClientError::ResponseEmpty("Job Metadata".into()))?
            .readout_mappings,
    })
}

/// API Errors encountered when trying to get Quil-T calibrations.
#[derive(Debug, thiserror::Error)]
pub enum GetQuiltCalibrationsError {
    /// Failed the http call
    #[error("Failed to get Quil-T calibrations via API: {0}")]
    ApiError(#[from] OpenAPIError<translation_api::GetQuiltCalibrationsError>),

    /// API call did not finish before timeout
    #[error("API call did not finish before timeout.")]
    TimeoutError(#[from] Elapsed),
}

/// Query the QCS API for Quil-T calibrations.
/// If `None`, the default `timeout` used is 10 seconds.
pub async fn get_quilt_calibrations(
    quantum_processor_id: &str,
    client: &Qcs,
    timeout: Option<Duration>,
) -> Result<GetQuiltCalibrationsResponse, GetQuiltCalibrationsError> {
    #[cfg(feature = "tracing")]
    tracing::debug!("getting Quil-T calibrations for {}", quantum_processor_id);

    let timeout = timeout.unwrap_or(DEFAULT_HTTP_API_TIMEOUT);

    tokio::time::timeout(timeout, async move {
        Ok(translation_api::get_quilt_calibrations(
            &client.get_openapi_client(),
            quantum_processor_id,
        )
        .await?)
    })
    .await?
}

/// Options available for Quil program translation.
///
/// This wraps [`ApiTranslationOptions`] in order to improve the user experience,
/// because the structs auto-generated by `prost` can be clumsy to use directly.
#[allow(clippy::module_name_repetitions)]
#[derive(Clone, Debug, Default)]
pub struct TranslationOptions {
    inner: ApiTranslationOptions,
}

impl TranslationOptions {
    /// Use the first-generation translation backend available on QCS since 2018.
    pub fn use_backend_v1(&mut self) {
        self.inner.translation_backend =
            Some(TranslationBackend::V1(translation::BackendV1Options {}));
    }

    /// Use the second-generation translation backend available on QCS since 2023
    pub fn use_backend_v2(&mut self) {
        self.inner.translation_backend =
            Some(TranslationBackend::V2(translation::BackendV2Options {}));
    }
}

impl From<TranslationOptions> for ApiTranslationOptions {
    fn from(options: TranslationOptions) -> Self {
        options.inner
    }
}
