use crate::{
    DeviceProperties,
    api::{endpoint::Requester, form::Form},
    consts,
    error::{
        CheckinError, FetchAuthError, IntoGoogleError, PropsError, TocError, UploadDeviceError,
    },
    proto,
};
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
use prost::Message;
use std::{
    borrow::Cow,
    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};

pub async fn checkin(
    props: &DeviceProperties,
) -> Result<proto::AndroidCheckinResponse, CheckinError> {
    let mut request = proto::AndroidCheckinRequest {
        device_configuration: Some(props.device_config.clone()),
        checkin: Some(props.android_checkin.clone()),

        time_zone: Some(consts::TIMEZONE.to_string()),
        locale: Some(consts::LOCALE.to_string()),

        version: Some(3),
        fragment: Some(0),
        id: Some(0),

        ..Default::default()
    };

    // ugly but i have to get a mut reference inside to update the timestamp
    // then generate the request as bytes
    // then grab another reference for a &str
    let body = {
        let checkin = request.checkin.as_mut().ok_or(CheckinError::NoDevice())?; // shouldn't happen
        let build = checkin.build.as_mut().ok_or(CheckinError::NoBuild())?;

        let seconds_unix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
        build.timestamp = Some(seconds_unix.cast_signed());
        request.encode_to_vec()
    };

    let headers = [
        ("content-type", "application/x-protobuf"),
        ("host", "android.clients.google.com"),
    ];

    let request = Requester::new()
        .props(props)
        .headers(&headers)
        .google_auth_headers()?;

    let response = request.generic_request("checkin", Some(body)).await?;
    let bytes = response.bytes().await?;

    let response = proto::AndroidCheckinResponse::decode(bytes)?;
    Ok(response)
}

pub async fn upload_device(
    props: &DeviceProperties,
    gsf: i64,
    checkin_token: &str,
) -> Result<proto::UploadDeviceConfigResponse, UploadDeviceError> {
    let request = proto::UploadDeviceConfigRequest {
        device_configuration: Some(props.device_config.clone()),
        ..Default::default()
    };

    let body = request.encode_to_vec();
    let headers = [("content-type", "application/x-protobuf")];

    let request = Requester::new()
        .props(props)
        .gsf(gsf)
        .checkin_token(checkin_token)
        .headers(&headers)
        .default_headers()
        .await?;

    let mut wrapper = request
        .fdfe_request("uploadDeviceConfig", Some(body))
        .await?;

    let response = wrapper
        .payload
        .take()
        .and_then(|a| a.upload_device_config_response)
        .into_google_error(&wrapper)?;

    Ok(response)
}

pub struct AuthToken(RwLock<InnerAuth>);
struct InnerAuth {
    token: String,
    expire: Instant,
}

impl AuthToken {
    #[must_use]
    pub const fn new(token: String, expire: Instant) -> Self {
        Self(RwLock::new(InnerAuth { token, expire }))
    }

    #[must_use]
    pub fn get(&'_ self) -> Option<MappedRwLockReadGuard<'_, str>> {
        let read_guard = self.0.read();

        if Instant::now() >= read_guard.expire {
            None
        } else {
            Some(RwLockReadGuard::map(read_guard, |a| &*a.token))
        }
    }

    pub fn update(&self, other: &Self) {
        let mut write_guard = self.0.write();
        let mut other_read = other.0.write();

        write_guard.token = std::mem::take(&mut other_read.token);
        write_guard.expire = std::mem::replace(&mut other_read.expire, Instant::now());
    }
}

pub async fn fetch_auth(
    props: &DeviceProperties,
    aas_token: &str,
    email: &str,
    gsf: i64,
) -> Result<AuthToken, FetchAuthError> {
    let build = props
        .android_checkin
        .build
        .as_ref()
        .ok_or(FetchAuthError::Props(PropsError::NoBuild()))?;

    let sdk_version = build
        .sdk_version
        .ok_or(FetchAuthError::Props(PropsError::NoBuildSDK()))?
        .to_string();

    let services_version = build
        .google_services
        .ok_or(FetchAuthError::Props(PropsError::NoVersion()))?
        .to_string(); // tired

    let language = consts::LANGUAGE.to_ascii_lowercase();
    let gsf = format!("{gsf:x}");

    let params = [
        (
            "service",
            "oauth2:https://www.googleapis.com/auth/googleplay",
        ),
        ("google_play_services_version", &services_version),
        ("device_country", consts::COUNTRY_CODE),
        ("callerPkg", consts::ANDROID_VENDING),
        ("client_sig", consts::CLIENT_SIG),
        ("app", consts::ANDROID_VENDING2),
        ("lang", &language),
        ("callerSig", consts::CALLER_SIG),
        ("token_request_options", "CAA4AVAB"),
        ("sdk_version", &sdk_version),
        ("oauth2_foreground", "1"),
        ("system_partition", "1"),
        ("androidId", &gsf),
        ("check_email", "1"),
        ("Token", aas_token),
        ("Email", email),
    ];

    let headers = [("content-length", "0")];
    let request = Requester::new()
        .props(props)
        .aas_token(aas_token)
        .email(email)
        .params(&params)
        .headers(&headers)
        .google_auth_headers()?;

    let mut response = request.form_request("auth", Form::new([])).await?;

    let token = response.remove("Auth").into_google_error_map(|| {
        response
            .remove("Error")
            .unwrap_or(Cow::Borrowed("Google returned no error!"))
            .to_string()
    })?;

    let secs = response
        .remove("ExpiresInDurationSec")
        .map(|a| a.parse::<u64>())
        .ok_or(FetchAuthError::NoExpireTime())??;

    Ok(AuthToken::new(
        token.to_string(),
        Instant::now() + Duration::from_secs(secs),
    ))
}

pub async fn toc(
    auth_token: &AuthToken,
    gsf: i64,
    checkin_token: &str,
    props: &DeviceProperties,
) -> Result<String, TocError> {
    let request = Requester::new()
        .auth_token(auth_token)
        .gsf(gsf)
        .checkin_token(checkin_token)
        .props(props)
        .default_headers()
        .await?;

    let mut wrapper = request.fdfe_request("toc", None).await?;

    let toc_response = wrapper
        .payload
        .take()
        .and_then(|a| a.toc_response)
        .into_google_error(&wrapper)?;

    if let Some(token) = toc_response.tos_token {
        let form = Form::new([("tost", &token), ("toscme", "false")]);

        // sends a form and returns a wrapper, lovely
        let mut tos_wrapper = request
            .fdfe_request("acceptTos", Some(form.format().into_bytes()))
            .await?;

        tos_wrapper
            .payload
            .take()
            .and_then(|a| a.accept_tos_response)
            .into_google_error(&tos_wrapper)?;
    }

    let response = toc_response.cookie.into_google_error(&wrapper)?;
    Ok(response)
}
