use serde::Deserialize;
use std::time::Duration;

const DEFAULT_LOGGING_FORMAT: &str = "pretty";
const DEFAULT_LOGGING_ENVIRONMENT: &str = "local";

#[derive(Debug, Clone, Deserialize)]
pub(crate) struct LoggingConfig {
    #[serde(default = "default_logging_format")]
    pub(crate) format: String,
    #[serde(default = "default_wide_events_enabled")]
    pub(crate) wide_events: bool,
    #[serde(default = "default_logging_environment")]
    pub(crate) environment: String,
    #[serde(default)]
    pub(crate) region: Option<String>,
    #[serde(default)]
    pub(crate) deployment_id: Option<String>,
    #[serde(default)]
    pub(crate) instance_id: Option<String>,
    #[serde(default)]
    pub(crate) commit_hash: Option<String>,
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            format: default_logging_format(),
            wide_events: default_wide_events_enabled(),
            environment: default_logging_environment(),
            region: None,
            deployment_id: None,
            instance_id: None,
            commit_hash: None,
        }
    }
}

pub(crate) fn default_logging_format() -> String {
    DEFAULT_LOGGING_FORMAT.to_string()
}

pub(crate) fn default_wide_events_enabled() -> bool {
    true
}

pub(crate) fn default_logging_environment() -> String {
    DEFAULT_LOGGING_ENVIRONMENT.to_string()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LoggingFormat {
    Pretty,
    Json,
}

impl LoggingFormat {
    pub(crate) fn parse(value: &str) -> Result<Self, String> {
        match value.trim().to_ascii_lowercase().as_str() {
            "" | "pretty" | "human" | "text" => Ok(Self::Pretty),
            "json" => Ok(Self::Json),
            other => Err(format!(
                "unsupported observability.logging.format '{other}'; expected 'pretty' or 'json'"
            )),
        }
    }

    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Pretty => "pretty",
            Self::Json => "json",
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct LoggingRuntime {
    pub(crate) format: LoggingFormat,
    pub(crate) wide_events_enabled: bool,
    pub(crate) service_name: String,
    pub(crate) service_version: String,
    pub(crate) environment: String,
    pub(crate) region: Option<String>,
    pub(crate) deployment_id: Option<String>,
    pub(crate) instance_id: Option<String>,
    pub(crate) commit_hash: Option<String>,
}

impl LoggingRuntime {
    pub(crate) fn from_config(config: Option<&LoggingConfig>) -> Result<Self, String> {
        let config = config.cloned().unwrap_or_default();
        Ok(Self {
            format: LoggingFormat::parse(config.format.as_str())?,
            wide_events_enabled: config.wide_events,
            service_name: env!("CARGO_PKG_NAME").to_string(),
            service_version: env!("CARGO_PKG_VERSION").to_string(),
            environment: normalize_required_string(
                Some(config.environment.as_str()),
                DEFAULT_LOGGING_ENVIRONMENT,
            ),
            region: normalize_optional_string(config.region.as_deref()),
            deployment_id: normalize_optional_string(config.deployment_id.as_deref()),
            instance_id: normalize_optional_string(config.instance_id.as_deref()),
            commit_hash: normalize_optional_string(config.commit_hash.as_deref()),
        })
    }
}

fn normalize_required_string(value: Option<&str>, fallback: &str) -> String {
    value
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
        .unwrap_or_else(|| fallback.to_string())
}

fn normalize_optional_string(value: Option<&str>) -> Option<String> {
    value
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_string)
}

pub(crate) struct WideRequestEventInput<'a> {
    pub(crate) request_id: &'a str,
    pub(crate) trace_id: &'a str,
    pub(crate) method: &'a str,
    pub(crate) route: &'a str,
    pub(crate) path: &'a str,
    pub(crate) matched_path: Option<&'a str>,
    pub(crate) host: Option<&'a str>,
    pub(crate) tenant: Option<&'a str>,
    pub(crate) origin: Option<&'a str>,
    pub(crate) referer: Option<&'a str>,
    pub(crate) user_agent: Option<&'a str>,
    pub(crate) query_present: bool,
    pub(crate) disable_cookie_cache: Option<&'a str>,
    pub(crate) status: u16,
    pub(crate) duration: Duration,
    pub(crate) request_body_bytes: Option<u64>,
    pub(crate) response_body_bytes: Option<u64>,
    pub(crate) authorization_header_present: bool,
    pub(crate) cookie_header_present: bool,
    pub(crate) session_cookie_name: &'a str,
    pub(crate) session_cookie_present: bool,
    pub(crate) set_cookie: bool,
    pub(crate) metrics_enabled: bool,
    pub(crate) loki_enabled: bool,
}

fn status_class(status: u16) -> &'static str {
    match status {
        100..=199 => "1xx",
        200..=299 => "2xx",
        300..=399 => "3xx",
        400..=499 => "4xx",
        500..=599 => "5xx",
        _ => "unknown",
    }
}

fn outcome(status: u16) -> &'static str {
    match status {
        500..=599 => "server_error",
        400..=499 => "client_error",
        300..=399 => "redirect",
        _ => "success",
    }
}

pub(crate) fn emit_request_wide_event(runtime: &LoggingRuntime, input: WideRequestEventInput<'_>) {
    if !runtime.wide_events_enabled {
        return;
    }

    let duration_ms = input.duration.as_secs_f64() * 1000.0;
    let status_class = status_class(input.status);
    let outcome = outcome(input.status);

    macro_rules! emit {
        ($macro:path) => {
            $macro!(
                event_type = "http_request",
                event_schema = "athena_auth.http_request.v1",
                service = %runtime.service_name,
                service_version = %runtime.service_version,
                environment = %runtime.environment,
                region = ?runtime.region,
                deployment_id = ?runtime.deployment_id,
                instance_id = ?runtime.instance_id,
                commit_hash = ?runtime.commit_hash,
                request_id = %input.request_id,
                trace_id = %input.trace_id,
                method = %input.method,
                route = %input.route,
                path = %input.path,
                matched_path = ?input.matched_path,
                host = ?input.host,
                tenant = ?input.tenant,
                origin = ?input.origin,
                referer = ?input.referer,
                user_agent = ?input.user_agent,
                query_present = input.query_present,
                disable_cookie_cache = ?input.disable_cookie_cache,
                status = input.status,
                status_class,
                outcome,
                duration_ms,
                request_body_bytes = ?input.request_body_bytes,
                response_body_bytes = ?input.response_body_bytes,
                authorization_header_present = input.authorization_header_present,
                cookie_header_present = input.cookie_header_present,
                session_cookie_name = %input.session_cookie_name,
                session_cookie_present = input.session_cookie_present,
                set_cookie = input.set_cookie,
                metrics_enabled = input.metrics_enabled,
                loki_enabled = input.loki_enabled,
                "http request wide event"
            );
        };
    }

    if input.status >= 500 {
        emit!(tracing::error);
    } else {
        emit!(tracing::info);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn logging_format_parser_accepts_pretty_and_json() {
        assert_eq!(LoggingFormat::parse("pretty"), Ok(LoggingFormat::Pretty));
        assert_eq!(LoggingFormat::parse("json"), Ok(LoggingFormat::Json));
        assert_eq!(LoggingFormat::parse("text"), Ok(LoggingFormat::Pretty));
    }

    #[test]
    fn logging_format_parser_rejects_unknown_values() {
        let err = LoggingFormat::parse("yaml").expect_err("unknown format should fail");
        assert!(err.contains("observability.logging.format"));
    }

    #[test]
    fn logging_runtime_defaults_to_local_pretty_wide_events() {
        let runtime = LoggingRuntime::from_config(None).expect("defaults should parse");
        assert_eq!(runtime.format, LoggingFormat::Pretty);
        assert!(runtime.wide_events_enabled);
        assert_eq!(runtime.environment, "local");
    }

    #[test]
    fn status_helpers_follow_http_classes() {
        assert_eq!(status_class(201), "2xx");
        assert_eq!(status_class(404), "4xx");
        assert_eq!(status_class(503), "5xx");
        assert_eq!(outcome(204), "success");
        assert_eq!(outcome(302), "redirect");
        assert_eq!(outcome(422), "client_error");
        assert_eq!(outcome(500), "server_error");
    }
}
