//! Directory endpoint.
use serde::{Deserialize, Serialize};
use serde_repr::Deserialize_repr;

use super::endpoint::{HttpsEndpointError, TIMEOUT, https_headers_with_authentication};
use crate::{
    common::{
        ChatServerGroup, ClientInfo, FeatureMask, ThreemaId,
        config::{DirectoryServerBaseUrl, Flavor, WorkContext},
        keys::{ClientKey, PublicKey},
    },
    crypto::digest::Mac as _,
    https::{HttpsHeadersBuilder, HttpsMethod, HttpsRequest, HttpsResponse, HttpsResult},
    model::contact::ContactInit,
    protobuf::{self, d2d_sync::contact as protobuf_contact},
    utils::{serde::base64, time::utc_now_ms},
};

#[derive(Serialize)]
struct WorkCredentials<'creds> {
    #[serde(rename = "licenseUsername")]
    username: &'creds str,

    #[serde(rename = "licensePassword")]
    password: &'creds str,
}
impl<'creds> From<&'creds crate::common::config::WorkCredentials> for WorkCredentials<'creds> {
    fn from(credentials: &'creds crate::common::config::WorkCredentials) -> Self {
        Self {
            username: &credentials.username,
            password: &credentials.password,
        }
    }
}

#[inline]
fn https_headers(mode: &Flavor) -> HttpsHeadersBuilder {
    match mode {
        Flavor::Consumer => HttpsHeadersBuilder::default(),
        Flavor::Work(context) => https_headers_with_authentication(context),
    }
}

fn handle_status<TStatusFn: FnOnce(u16) -> Option<HttpsEndpointError>>(
    result: HttpsResult,
    unexpected_status_map_fn: TStatusFn,
) -> Result<HttpsResponse, HttpsEndpointError> {
    let response = result?;
    match response.status {
        200 | 204 => Ok(response),
        401 => Err(HttpsEndpointError::InvalidCredentials),
        429 => Err(HttpsEndpointError::RateLimitExceeded),
        status => {
            Err(unexpected_status_map_fn(status).unwrap_or(HttpsEndpointError::UnexpectedStatus(status)))
        },
    }
}

#[derive(Deserialize)]
#[serde(untagged)]
enum AwkwardResponse {
    Error {
        #[serde(rename = "success")]
        success: bool,
        #[serde(rename = "error")]
        error: String,
    },
    Success {
        #[serde(rename = "success")]
        success: bool,
    },
}

fn handle_status_and_awkward_response<TStatusFn: FnOnce(u16) -> Option<HttpsEndpointError>>(
    result: HttpsResult,
    unexpected_status_map_fn: TStatusFn,
) -> Result<HttpsResponse, HttpsEndpointError> {
    let response = handle_status(result, unexpected_status_map_fn)?;

    // Handle the super-awkward response with the `success` field
    if response.status != 200 {
        return Err(HttpsEndpointError::UnexpectedStatus(response.status));
    }
    match serde_json::from_slice::<AwkwardResponse>(&response.body)? {
        AwkwardResponse::Error {
            success: _success,
            error,
        } => Err(HttpsEndpointError::CustomPossiblyLocalizedError(error)),
        AwkwardResponse::Success { success } => {
            if success {
                Ok(response)
            } else {
                Err(HttpsEndpointError::CustomPossiblyLocalizedError(
                    "Server is totally stoned".to_owned(),
                ))
            }
        },
    }
}

#[derive(Deserialize)]
struct AuthenticationChallenge {
    #[serde(rename = "tokenRespKeyPub", with = "base64::fixed_length")]
    public_key: [u8; PublicKey::LENGTH],

    #[serde(rename = "token", with = "base64::variable_length")]
    challenge: Vec<u8>,
}

#[derive(Serialize)]
pub(crate) struct AuthenticationChallengeResponse {
    #[serde(rename = "token", with = "base64::variable_length")]
    challenge: Vec<u8>,

    #[serde(rename = "response", with = "base64::fixed_length")]
    response: [u8; PublicKey::LENGTH],
}

/// Process the result and solve the authentication challenge.
pub(crate) fn handle_authentication_challenge(
    client_key: &ClientKey,
    result: HttpsResult,
) -> Result<AuthenticationChallengeResponse, HttpsEndpointError> {
    let response = handle_status(result, |_| None)?;
    let challenge: AuthenticationChallenge = serde_json::from_slice(&response.body)?;
    let response = AuthenticationChallengeResponse {
        challenge: challenge.challenge.clone(),
        response: client_key
            .derive_directory_authentication_key(&PublicKey::from(challenge.public_key))
            .ok_or(HttpsEndpointError::InvalidChallenge)?
            .0
            .chain_update(&challenge.challenge)
            .finalize()
            .into_bytes()
            .into(),
    };
    Ok(response)
}

#[derive(Serialize)]
struct IdentitiesRequest<'request> {
    identities: &'request [ThreemaId],
}

/// Request identity properties tied to a set of identities.
pub(crate) fn request_identities(
    client_info: &ClientInfo,
    directory_server_url: &DirectoryServerBaseUrl,
    mode: &Flavor,
    identities: &[ThreemaId],
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: directory_server_url.request_identities_path(),
        method: HttpsMethod::Post,
        headers: https_headers(mode).accept("application/json").build(client_info),
        body: serde_json::to_vec(&IdentitiesRequest { identities })
            .expect("Failed to create directory identities request body"),
    }
}

#[derive(Deserialize_repr)]
#[repr(u8)]
enum IdentityType {
    Regular = 0,
    Work = 1,
}
impl From<IdentityType> for protobuf_contact::IdentityType {
    fn from(r#type: IdentityType) -> Self {
        match r#type {
            IdentityType::Regular => protobuf_contact::IdentityType::Regular,
            IdentityType::Work => protobuf_contact::IdentityType::Work,
        }
    }
}

#[derive(Default, Deserialize_repr)]
#[repr(u8)]
enum ActivityState {
    #[default]
    Active = 0,
    Inactive = 1,
}
impl From<ActivityState> for protobuf_contact::ActivityState {
    fn from(state: ActivityState) -> Self {
        match state {
            ActivityState::Active => protobuf_contact::ActivityState::Active,
            ActivityState::Inactive => protobuf_contact::ActivityState::Inactive,
        }
    }
}

#[derive(Deserialize)]
struct ValidIdentity {
    #[serde(rename = "identity")]
    identity: ThreemaId,

    #[serde(rename = "type")]
    identity_type: IdentityType,

    #[serde(rename = "publicKey", with = "base64::fixed_length")]
    public_key: [u8; PublicKey::LENGTH],

    // Note: OnPrem directory omits this parameter for some reason, so we need a default
    #[serde(default, rename = "state")]
    activity_state: ActivityState,

    #[serde(rename = "featureMask")]
    feature_mask: u64,
}
impl From<ValidIdentity> for ContactInit {
    fn from(entry: ValidIdentity) -> Self {
        ContactInit {
            identity: entry.identity,
            public_key: PublicKey::from(entry.public_key),
            created_at: utc_now_ms(),
            first_name: None,
            last_name: None,
            nickname: None,
            verification_level: protobuf_contact::VerificationLevel::Unverified,
            work_verification_level: protobuf_contact::WorkVerificationLevel::None,
            identity_type: entry.identity_type.into(),
            acquaintance_level: protobuf_contact::AcquaintanceLevel::GroupOrDeleted,
            activity_state: entry.activity_state.into(),
            feature_mask: FeatureMask(entry.feature_mask),
            sync_state: protobuf_contact::SyncState::Initial,
            work_last_full_sync_at: None,
            work_availability_status: None,
            read_receipt_policy_override: None,
            typing_indicator_policy_override: None,
            notification_trigger_policy_override: None,
            conversation_category: protobuf::d2d_sync::ConversationCategory::Default,
            conversation_visibility: protobuf::d2d_sync::ConversationVisibility::Normal,
        }
    }
}

#[derive(Deserialize)]
struct IdentitiesResponse {
    #[serde(rename = "identities")]
    identities: Vec<ValidIdentity>,
}

/// Process the identities result and map it to all valid identities.
///
/// IMPORTANT: Identities that do not exist or have already been revoked will not be included!
pub(crate) fn handle_identities_result(result: HttpsResult) -> Result<Vec<ContactInit>, HttpsEndpointError> {
    let response = handle_status(result, |_| None)?;
    let IdentitiesResponse { identities } = serde_json::from_slice(&response.body)?;
    Ok(identities.into_iter().map(ContactInit::from).collect())
}

#[derive(Serialize)]
struct CreateIdentityRequest {
    #[serde(rename = "publicKey", with = "base64::fixed_length")]
    public_key: [u8; PublicKey::LENGTH],
}

/// Request an authentication challenge to create an identity.
pub(crate) fn create_identity_authentication_request(
    client_info: &ClientInfo,
    directory_server_url: &DirectoryServerBaseUrl,
    mode: &Flavor,
    public_key: PublicKey,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: directory_server_url.create_identity_path(),
        method: HttpsMethod::Post,
        headers: https_headers(mode).accept("application/json").build(client_info),
        body: serde_json::to_vec(&CreateIdentityRequest {
            public_key: public_key.0.to_bytes(),
        })
        .expect("Failed to create directory identity creation challenge request body"),
    }
}

#[derive(Serialize)]
struct CreateIdentityAuthenticatedRequest<'creds> {
    #[serde(flatten)]
    request: CreateIdentityRequest,

    #[serde(flatten)]
    authentication: AuthenticationChallengeResponse,

    #[serde(flatten)]
    credentials: Option<WorkCredentials<'creds>>,
}

/// Create an identity.
pub(crate) fn create_identity_request(
    client_info: &ClientInfo,
    directory_server_url: &DirectoryServerBaseUrl,
    mode: &Flavor,
    authentication: AuthenticationChallengeResponse,
    public_key: PublicKey,
) -> HttpsRequest {
    let credentials = match mode {
        Flavor::Work(work_context) => Some(WorkCredentials::from(&work_context.credentials)),
        Flavor::Consumer => None,
    };
    HttpsRequest {
        timeout: TIMEOUT,
        url: directory_server_url.create_identity_path(),
        method: HttpsMethod::Post,
        headers: https_headers(mode).accept("application/json").build(client_info),
        body: serde_json::to_vec(&CreateIdentityAuthenticatedRequest {
            request: CreateIdentityRequest {
                public_key: public_key.0.to_bytes(),
            },
            authentication,
            credentials,
        })
        .expect("Failed to create directory identity creation request body"),
    }
}

/// Response for a newly created identity.
#[derive(Deserialize)]
pub(crate) struct CreateIdentityResponse {
    #[serde(rename = "identity")]
    pub(crate) identity: ThreemaId,

    #[serde(rename = "serverGroup")]
    pub(crate) server_group: ChatServerGroup,
}

/// Process the result after attempting to create an identity.
pub(crate) fn handle_create_identity_result(
    result: HttpsResult,
) -> Result<CreateIdentityResponse, HttpsEndpointError> {
    let response = handle_status_and_awkward_response(result, |_| None)?;
    Ok(serde_json::from_slice(&response.body)?)
}

#[derive(Serialize)]
struct UpdateWorkPropertiesRequest<'body> {
    #[serde(flatten)]
    credentials: WorkCredentials<'body>,

    #[serde(rename = "identity")]
    identity: ThreemaId,

    #[serde(rename = "version")]
    version: &'body str,
}

/// Request an authentication challenge to update work properties.
///
/// Note: Technically, this is a part that the work directory should do but historically it is part of the
/// directory. So, we're reflecting that... for now.
pub(crate) fn update_work_properties_authentication_request(
    client_info: &ClientInfo,
    directory_server_url: &DirectoryServerBaseUrl,
    work_context: &WorkContext,
    identity: ThreemaId,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: directory_server_url.update_work_properties_path(),
        method: HttpsMethod::Post,
        headers: https_headers_with_authentication(work_context)
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&UpdateWorkPropertiesRequest {
            credentials: (&work_context.credentials).into(),
            identity,
            version: &client_info.to_semicolon_separated(),
        })
        .expect("Failed to create update work properties (legacy) challenge request body"),
    }
}

#[derive(Serialize)]
struct UpdateWorkPropertiesAuthenticatedRequest<'body> {
    #[serde(flatten)]
    request: UpdateWorkPropertiesRequest<'body>,

    #[serde(flatten)]
    authentication: AuthenticationChallengeResponse,
}

/// Update work properties.
///
/// Note: Technically, this is a part that the work directory should do but historically it is part of the
/// directory. So, we're reflecting that... for now.
pub(crate) fn update_work_properties_request(
    client_info: &ClientInfo,
    directory_server_url: &DirectoryServerBaseUrl,
    work_context: &WorkContext,
    identity: ThreemaId,
    authentication: AuthenticationChallengeResponse,
) -> HttpsRequest {
    HttpsRequest {
        timeout: TIMEOUT,
        url: directory_server_url.update_work_properties_path(),
        method: HttpsMethod::Post,
        headers: https_headers_with_authentication(work_context)
            .accept("application/json")
            .build(client_info),
        body: serde_json::to_vec(&UpdateWorkPropertiesAuthenticatedRequest {
            request: UpdateWorkPropertiesRequest {
                credentials: (&work_context.credentials).into(),
                identity,
                version: &client_info.to_semicolon_separated(),
            },
            authentication,
        })
        .expect("Failed to create update work properties (legacy) request body"),
    }
}

/// Process the result after attempting to update work properties.
pub(crate) fn handle_update_work_properties_result(result: HttpsResult) -> Result<(), HttpsEndpointError> {
    let _ = handle_status_and_awkward_response(result, |_| None)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;
    use serde_json::json;

    use crate::{
        common::keys::ClientKey,
        https::{HttpsResponse, directory::handle_authentication_challenge, endpoint::HttpsEndpointError},
        utils::test::ResultUnwrapErrorQuiet as _,
    };

    #[test]
    fn authentication_challenge_with_non_contributory_public_key() {
        let error = handle_authentication_challenge(
            &ClientKey::from([0; ClientKey::LENGTH]),
            Ok(HttpsResponse {
                status: 200,
                body: serde_json::to_vec(&json!({
                    "tokenRespKeyPub": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
                    "token": "AQID",
                }))
                .unwrap(),
            }),
        )
        .unwrap_err_quiet();
        assert_matches!(error, HttpsEndpointError::InvalidChallenge);
    }
}
