/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use super::glean_metrics;
use anyhow::Context;
use glean::private::{
    BooleanMetric, DatetimeMetric, ObjectMetric, QuantityMetric, StringListMetric, StringMetric,
    TimespanMetric,
};
use glean::TestGetValue;

pub struct Annotation {
    pub key: &'static str,
    pub glean_key: &'static str,
    #[cfg(test)]
    pub convert_fn: &'static str,
    pub set_glean_metric: fn(&serde_json::Value) -> anyhow::Result<()>,
    pub test_get_glean_value: fn() -> Option<serde_json::Value>,
}

macro_rules! convert {
    ( $category:ident :: $metric:ident = $func:ident ($key:literal $(, $($args:expr),*)? ) ) => {
        Annotation {
            key: $key,
            glean_key: concat!(stringify!($category), ".", stringify!($metric)),
            #[cfg(test)]
            convert_fn: stringify!($func),
            set_glean_metric: |value: &serde_json::Value| -> anyhow::Result<()> {
                SourceValue(value).try_into().map_err(|e| anyhow::Error::from(e))
                    .and_then(|v| $func(&glean_metrics::$category::$metric, v $(, $($args),*)? ))
                    .context(concat!("while trying to set ", stringify!($category), "::", stringify!($metric), " from annotation ", $key))
            },
            test_get_glean_value: || -> Option<serde_json::Value> {
                glean_metrics::$category::$metric.test_get_value(Some("crash".into()))
                    .map(|v| TestGetValueToJson(v).into())
            }
        }
    };
}

// Env variable set to the file generated by conversions.py (by build.rs).
include!(env!("CONVERSIONS_FILE"));

struct SourceValue<'a>(&'a serde_json::Value);

impl<'a> TryFrom<SourceValue<'a>> for &'a str {
    type Error = anyhow::Error;

    fn try_from(value: SourceValue<'a>) -> Result<Self, Self::Error> {
        value.0.as_str().context("expected a string")
    }
}

impl<'a> TryFrom<SourceValue<'a>> for &'a serde_json::Value {
    type Error = std::convert::Infallible;

    fn try_from(value: SourceValue<'a>) -> Result<Self, Self::Error> {
        Ok(value.0)
    }
}

struct TestGetValueToJson<T>(T);

macro_rules! TestGetValueToJson_passthrough {
    ( $($T:ty),* ) => {
        $(
        impl From<TestGetValueToJson<$T>> for serde_json::Value {
            fn from(value: TestGetValueToJson<$T>) -> Self {
                value.0.into()
            }
        })*
    }
}

TestGetValueToJson_passthrough![serde_json::Value, String, bool, i64, Vec<String>];

impl From<TestGetValueToJson<glean::Datetime>> for serde_json::Value {
    fn from(value: TestGetValueToJson<glean::Datetime>) -> Self {
        let glean::Datetime {
            year,
            month,
            day,
            hour,
            minute,
            second,
            nanosecond,
            offset_seconds,
        } = value.0;

        time::OffsetDateTime::new_in_offset(
            time::Date::from_calendar_date(year, (month as u8).try_into().unwrap(), day as u8)
                .unwrap(),
            time::Time::from_hms_nano(hour as u8, minute as u8, second as u8, nanosecond).unwrap(),
            time::UtcOffset::from_whole_seconds(offset_seconds).unwrap(),
        )
        .to_string()
        .into()
    }
}

fn convert_boolean_to_boolean(metric: &BooleanMetric, value: &str) -> anyhow::Result<()> {
    metric.set(value == "1");
    Ok(())
}

fn convert_string_to_string(metric: &StringMetric, value: &str) -> anyhow::Result<()> {
    metric.set(value.to_owned());
    Ok(())
}

fn convert_u64_to_quantity(metric: &QuantityMetric, value: &str) -> anyhow::Result<()> {
    metric.set(value.parse().context("couldn't parse quantity")?);
    Ok(())
}

fn convert_string_to_string_list(
    metric: &StringListMetric,
    value: &str,
    delimiter: &str,
) -> anyhow::Result<()> {
    metric.set(
        value
            .split(delimiter)
            .filter(|s| !s.is_empty())
            .map(|s| s.to_owned())
            .collect(),
    );
    Ok(())
}

fn convert_to_crash_time(metric: &DatetimeMetric, value: &str) -> anyhow::Result<()> {
    let seconds_since_epoch: i64 = value
        .parse()
        .context("failed to parse crash time seconds")?;
    metric.set(Some(glean_datetime(
        time::OffsetDateTime::from_unix_timestamp(seconds_since_epoch)
            .context("failed to convert crash time unix timestamp")?,
    )));
    Ok(())
}

fn convert_duration_seconds(metric: &TimespanMetric, value: &str) -> anyhow::Result<()> {
    metric.set_raw(std::time::Duration::from_secs(
        value
            .parse()
            .context("failed to parse seconds since last crash")?,
    ));
    Ok(())
}

fn convert_to_environment_uptime(metric: &TimespanMetric, value: &str) -> anyhow::Result<()> {
    metric.set_raw(std::time::Duration::from_secs_f32(
        value.parse().context("failed to parse uptime")?,
    ));
    Ok(())
}

fn convert_to_crash_windows_file_dialog_error_code(
    metric: &StringMetric,
    value: &str,
) -> anyhow::Result<()> {
    metric.set(value.to_owned()); // Just assume it is an integer as expected
    Ok(())
}

fn convert_to_crash_async_shutdown_timeout(
    metric: &ObjectMetric<glean_metrics::crash::AsyncShutdownTimeoutObject>,
    value: &str,
) -> anyhow::Result<()> {
    // The JSON value is stored as a string, so we need to deserialize it.
    let mut value: serde_json::Map<String, serde_json::Value> = serde_json::from_str(value)?;
    metric.set(glean_metrics::crash::AsyncShutdownTimeoutObject {
        broken_add_blockers: {
            if let Some(broken_add_blockers) = value.remove("brokenAddBlockers") {
                let broken_add_blockers = broken_add_blockers
                    .as_array()
                    .context("expected array for brokenAddBlockers")?;
                let mut ret = Vec::with_capacity(broken_add_blockers.len());
                for entry in broken_add_blockers {
                    ret.push(
                        entry
                            .as_str()
                            .context("expected brokenAddBlockers entry to be a string")?
                            .to_owned(),
                    );
                }
                ret
            } else {
                Default::default()
            }
        },
        conditions: {
            if let Some(conditions) = value.remove("conditions") {
                Some(if let Some(s) = conditions.as_str() {
                    s.to_owned()
                } else {
                    serde_json::to_string(&conditions).context("failed to serialize conditions")?
                })
            } else {
                None
            }
        },
        phase: {
            if let Some(phase) = value.remove("phase") {
                Some(
                    phase
                        .as_str()
                        .context("expected phase to be a string")?
                        .to_owned(),
                )
            } else {
                None
            }
        },
    });
    Ok(())
}

fn convert_to_crash_quota_manager_shutdown_timeout(
    metric: &ObjectMetric<glean_metrics::crash::QuotaManagerShutdownTimeoutObject>,
    value: &str,
) -> anyhow::Result<()> {
    // The Glean metric is an array of the lines.
    metric.set(value.lines().map(|s| s.to_owned()).collect::<Vec<_>>());
    Ok(())
}

fn convert_to_crash_stack_traces(
    metric: &ObjectMetric<glean_metrics::crash::StackTracesObject>,
    value: &str,
) -> anyhow::Result<()> {
    // The JSON value is stored as a string, so we need to deserialize it.
    let value: serde_json::Value = serde_json::from_str(value)?;
    let error = value["error"].as_str().map(|s| s.to_owned());
    let crash_type = value["crash_type"].as_str().map(|s| s.to_owned());
    let crash_address = value["crash_address"].as_str().map(|s| s.to_owned());
    let crash_thread = value["crash_thread"].as_i64();
    let main_module = value["main_module"].as_i64();
    let modules = value["modules"]
        .as_array()
        .map(|modules| {
            modules
                .iter()
                .map(|m| glean_metrics::crash::StackTracesObjectModulesItem {
                    base_address: m["base_address"].as_str().map(|s| s.to_owned()),
                    end_address: m["end_address"].as_str().map(|s| s.to_owned()),
                    code_id: m["code_id"].as_str().map(|s| s.to_owned()),
                    debug_file: m["debug_file"].as_str().map(|s| s.to_owned()),
                    debug_id: m["debug_id"].as_str().map(|s| s.to_owned()),
                    filename: m["filename"].as_str().map(|s| s.to_owned()),
                    version: m["version"].as_str().map(|s| s.to_owned()),
                })
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();
    let threads = value["threads"]
        .as_array()
        .map(|threads| {
            threads
                .iter()
                .map(|t| glean_metrics::crash::StackTracesObjectThreadsItem {
                    frames: t["frames"]
                        .as_array()
                        .map(|frames| {
                            frames
                                .iter()
                                .map(|f| {
                                    glean_metrics::crash::StackTracesObjectThreadsItemFramesItem {
                                        module_index: f["module_index"].as_i64(),
                                        ip: f["ip"].as_str().map(|s| s.to_owned()),
                                        trust: f["trust"].as_str().map(|s| s.to_owned()),
                                    }
                                })
                                .collect::<Vec<_>>()
                        })
                        .unwrap_or_default(),
                })
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();
    metric.set(glean_metrics::crash::StackTracesObject {
        error,
        crash_type,
        crash_address,
        crash_thread,
        main_module,
        modules,
        threads,
    });
    Ok(())
}

// The format of the JSON value is defined in getStacktraceAsJsonString
// (mobile/android/android-components/components/support/base/src/main/java/mozilla/components/support/base/ext/Throwable.kt).
fn convert_to_crash_java_exception(
    metric: &ObjectMetric<glean_metrics::crash::JavaExceptionObject>,
    value: &str,
) -> anyhow::Result<()> {
    // The JSON value is stored as a string, so we need to deserialize it.
    let value: serde_json::Map<String, serde_json::Value> = serde_json::from_str(value)?;
    let throwables = value["exception"]["values"]
        .as_array()
        .context("expected throwables array")?
        .iter()
        // The format stores throwables in reverse order (deepest cause first), but we define the
        // glean format to be by top cause first.
        .rev()
        .map(|throwable| &throwable["stacktrace"])
        .map(
            |throwable| glean_metrics::crash::JavaExceptionObjectThrowablesItem {
                message: throwable["value"].as_str().map(ToOwned::to_owned),
                type_name: throwable["module"]
                    .as_str()
                    .zip(throwable["type"].as_str())
                    .map(|(m, t)| format!("{m}.{t}")),
                stack: throwable["frames"]
                    .as_array()
                    .map(|frames| {
                        frames
                            .iter()
                            .map(|frame| {
                                glean_metrics::crash::JavaExceptionObjectThrowablesItemStackItem {
                                    class_name: frame["module"].as_str().map(ToOwned::to_owned),
                                    method_name: frame["function"].as_str().map(ToOwned::to_owned),
                                    is_native: frame["in_app"].as_bool().map(|b| !b),
                                    line: frame["lineno"].as_i64(),
                                    file: frame["filename"].as_str().map(ToOwned::to_owned),
                                }
                            })
                            .collect()
                    })
                    .unwrap_or_default(),
            },
        )
        .collect();

    metric.set(glean_metrics::crash::JavaExceptionObject { throwables });
    Ok(())
}

fn glean_datetime(datetime: time::OffsetDateTime) -> glean::Datetime {
    glean::Datetime {
        year: datetime.year(),
        month: datetime.month() as _,
        day: datetime.day() as _,
        hour: datetime.hour() as _,
        minute: datetime.minute() as _,
        second: datetime.second() as _,
        nanosecond: datetime.nanosecond(),
        offset_seconds: datetime.offset().whole_seconds(),
    }
}
