// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use {
    crate::update::{config::Initiator, FetchError, PrepareError, ResolveError},
    cobalt_client::traits::AsEventCode,
    cobalt_sw_delivery_registry as metrics,
    futures::future::Future,
    std::{
        convert::TryInto,
        time::{Duration, Instant, SystemTime},
    },
};

// See $FUCHSIA_OUT_DIR/gen/src/sys/pkg/bin/amber/cobalt_sw_delivery_registry.rs for more info.
pub use {
    metrics::SoftwareDeliveryMetricDimensionPhase as Phase,
    metrics::SoftwareDeliveryMetricDimensionStatusCode as StatusCode,
};

pub fn connect_to_cobalt() -> (Client, impl Future<Output = ()>) {
    let (cobalt, cobalt_fut) = fuchsia_cobalt::CobaltConnector::default()
        .serve(fuchsia_cobalt::ConnectionType::project_id(metrics::PROJECT_ID));
    (Client(cobalt), cobalt_fut)
}

fn error_to_status_code(error: &fidl_fuchsia_pkg_ext::ResolveError) -> StatusCode {
    match *error {
        fidl_fuchsia_pkg_ext::ResolveError::Io => StatusCode::ErrorStorage,
        fidl_fuchsia_pkg_ext::ResolveError::NoSpace => StatusCode::ErrorStorageOutOfSpace,
        fidl_fuchsia_pkg_ext::ResolveError::AccessDenied => StatusCode::ErrorUntrustedTufRepo,
        fidl_fuchsia_pkg_ext::ResolveError::UnavailableBlob => StatusCode::ErrorNetworking,
        _ => StatusCode::Error,
    }
}

pub fn result_to_status_code(res: Result<(), &anyhow::Error>) -> StatusCode {
    match res {
        Ok(()) => StatusCode::Success,

        Err(e) => {
            if let Some(FetchError::Resolve(ResolveError::Error(error, _))) = e.downcast_ref() {
                error_to_status_code(error)
            } else if let Some(PrepareError::ResolveUpdate(ResolveError::Error(error, _))) =
                e.downcast_ref()
            {
                error_to_status_code(error)
            } else {
                // Fallback to a generic catch-all error status code when the error didn't contain
                // context indicating more clearly what type of error happened.
                StatusCode::Error
            }
        }
    }
}

impl AsEventCode for Initiator {
    fn as_event_code(&self) -> u32 {
        match self {
            Initiator::Automatic => {
                metrics::SoftwareDeliveryMetricDimensionInitiator::AutomaticUpdateCheck
            }
            Initiator::Manual => {
                metrics::SoftwareDeliveryMetricDimensionInitiator::UserInitiatedCheck
            }
        }
        .as_event_code()
    }
}

fn hour_of_day(when: SystemTime) -> u32 {
    use chrono::Timelike;

    let when = chrono::DateTime::<chrono::Utc>::from(when);
    // TODO insert UTC to local time conversion here...when that is possible to do.
    when.hour()
}

fn duration_to_micros(duration: Duration) -> i64 {
    duration.as_micros().try_into().unwrap_or(i64::MAX)
}

/// Attempts to determine the monotonic instant that correlates with the given wall time by
/// subtracting the duration from the given time to now (wall time) from now (monotonic time). If
/// `time` is in the future or it corresponds with an invalid monotonic time, returns None.
///
/// This conversion is flawed as there is no guarantee that the monotonic and wall clocks tick at
/// the same rate or that the system was up at the given wall time.
///
/// FIXME: switch start time to initially be based on monotonic time to remove the need for this
/// conversion.
pub fn system_time_to_monotonic_time(time: SystemTime) -> Option<Instant> {
    Instant::now().checked_sub(time.elapsed().ok()?)
}

#[derive(Debug, Clone)]
pub struct Client(fuchsia_cobalt::CobaltSender);

impl Client {
    pub fn log_ota_start(&mut self, target_version: &str, initiator: Initiator, when: SystemTime) {
        self.0.with_component().log_event_count(
            metrics::OTA_START_METRIC_ID,
            (initiator, hour_of_day(when)),
            target_version,
            0, // duration
            1, // count
        );
    }

    pub fn log_ota_result_attempt(
        &mut self,
        target_version: &str,
        initiator: Initiator,
        attempts: u32,
        phase: Phase,
        status: StatusCode,
    ) {
        self.0.with_component().log_event_count(
            metrics::OTA_RESULT_ATTEMPTS_METRIC_ID,
            (initiator, phase, status),
            target_version,
            0,
            attempts.into(), // "attempts" is not "count", but that's what the go impl does.
        );
    }

    pub fn log_ota_result_duration(
        &mut self,
        target_version: &str,
        initiator: Initiator,
        phase: Phase,
        status: StatusCode,
        duration: Duration,
    ) {
        self.0.with_component().log_elapsed_time(
            metrics::OTA_RESULT_DURATION_METRIC_ID,
            (initiator, phase, status),
            target_version,
            duration_to_micros(duration),
        );
    }
}

#[cfg(test)]
mod tests {
    use {super::*, chrono::prelude::*};

    #[test]
    fn hour_of_day_returns_hour_of_day() {
        let date = Utc.ymd(1971, 1, 1);

        assert_eq!(hour_of_day(date.and_hms(4, 0, 0).into()), 4);
        assert_eq!(hour_of_day(date.and_hms(23, 0, 0).into()), 23);
    }
}
