use std::convert::TryFrom;

use alloy::primitives::Address;
use axum::{Router, http::StatusCode, response::IntoResponse};
use once_cell::sync::Lazy;
use prometheus::{Encoder, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry};

// registry we can re-use
static REGISTRY: Lazy<Registry> = Lazy::new(Registry::new);

// metrics handles
static L2_BLOCKS_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
    IntCounter::new("l2_blocks_total", "Total number of l2 blocks observed")
        .expect("l2_blocks_total metric can be created")
});

static EJECTIONS_TOTAL: Lazy<IntCounterVec> = Lazy::new(|| {
    IntCounterVec::new(
        prometheus::opts!("ejections_total", "total number of ejections attempted"),
        &["status", "addr"],
    )
    .expect("ejections_total metric can be created")
});

static L1_EVENT_SCANNER_RESTARTS_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
    IntCounter::new(
        "l1_event_scanner_restarts_total",
        "Total number of HTTP whitelist event scanner restarts",
    )
    .expect("l1_event_scanner_restarts_total metric can be created")
});

static L2_POLL_UNCERTAIN_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
    IntCounter::new("l2_poll_uncertain_total", "Total number of uncertain HTTP L2 polling outcomes")
        .expect("l2_poll_uncertain_total metric can be created")
});

static LAST_SEEN_DRIFT_SECONDS: Lazy<IntGauge> = Lazy::new(|| {
    IntGauge::new(
        "last_seen_drift_seconds",
        "Seconds since the ejector watchdog last reset its timer",
    )
    .expect("last_seen_drift_seconds metric can be created")
});

static LAST_BLOCK_AGE_SECONDS: Lazy<IntGauge> = Lazy::new(|| {
    IntGauge::new("last_block_age_seconds", "Seconds since the ejector observed an L2 block header")
        .expect("last_block_age_seconds metric can be created")
});

static REORG_COUNT_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
    IntCounter::new("reorg_count_total", "Total number of L2 reorg events observed")
        .expect("reorg_count_total metric can be created")
});

static REORG_SKIPPED_TOTAL: Lazy<IntCounter> = Lazy::new(|| {
    IntCounter::new("reorg_skipped_total", "Reorg events observed but below the eject threshold")
        .expect("reorg_skipped_total metric can be created")
});

static REORG_DEPTH_BLOCKS: Lazy<IntGauge> = Lazy::new(|| {
    IntGauge::new("reorg_depth_blocks", "Number of blocks replaced in the most recent reorg event")
        .expect("reorg_depth_blocks metric can be created")
});

static LAST_REORGED_TO: Lazy<IntGauge> = Lazy::new(|| {
    IntGauge::new("last_reorged_to", "Revert height of the latest reorg")
        .expect("last_reorged_to metric can be created")
});

static PRECONFER_MISSING_FROM_WHITELIST: Lazy<IntGaugeVec> = Lazy::new(|| {
    IntGaugeVec::new(
        prometheus::opts!(
            "preconfer_missing_from_whitelist",
            "Whether a configured preconfer is missing from the active whitelist"
        ),
        &["addr"],
    )
    .expect("preconfer_missing_from_whitelist metric can be created")
});

pub fn init() {
    REGISTRY
        .register(Box::new(L2_BLOCKS_TOTAL.clone()))
        .expect("l2_blocks_total metric can be registered");
    REGISTRY
        .register(Box::new(EJECTIONS_TOTAL.clone()))
        .expect("ejections_total metric can be registered");
    REGISTRY
        .register(Box::new(L1_EVENT_SCANNER_RESTARTS_TOTAL.clone()))
        .expect("l1_event_scanner_restarts_total metric can be registered");
    REGISTRY
        .register(Box::new(L2_POLL_UNCERTAIN_TOTAL.clone()))
        .expect("l2_poll_uncertain_total metric can be registered");
    REGISTRY
        .register(Box::new(LAST_SEEN_DRIFT_SECONDS.clone()))
        .expect("last_seen_drift_seconds metric can be registered");
    REGISTRY
        .register(Box::new(LAST_BLOCK_AGE_SECONDS.clone()))
        .expect("last_block_age_seconds metric can be registered");
    REGISTRY
        .register(Box::new(REORG_COUNT_TOTAL.clone()))
        .expect("reorg_count_total metric can be registered");
    REGISTRY
        .register(Box::new(REORG_SKIPPED_TOTAL.clone()))
        .expect("reorg_skipped_total metric can be registered");
    REGISTRY
        .register(Box::new(REORG_DEPTH_BLOCKS.clone()))
        .expect("reorg_depth_blocks metric can be registered");
    REGISTRY
        .register(Box::new(LAST_REORGED_TO.clone()))
        .expect("last_reorged_to metric can be registered");
    REGISTRY
        .register(Box::new(PRECONFER_MISSING_FROM_WHITELIST.clone()))
        .expect("preconfer_missing_from_whitelist metric can be registered");
}

pub fn router() -> axum::Router {
    init();
    Router::new().route("/metrics", axum::routing::get(metrics_handler))
}

async fn metrics_handler() -> impl IntoResponse {
    let metrics = REGISTRY.gather();
    let encoder = prometheus::TextEncoder::new();
    let mut buffer = Vec::new();
    if let Err(e) = encoder.encode(&metrics, &mut buffer) {
        tracing::error!("could not encode prometheus metrics: {}", e);
        return (StatusCode::INTERNAL_SERVER_ERROR, "could not encode metrics").into_response();
    }

    (StatusCode::OK, [(axum::http::header::CONTENT_TYPE, encoder.format_type())], buffer)
        .into_response()
}

// public functions to increment metrics
pub fn inc_l2_blocks() {
    L2_BLOCKS_TOTAL.inc();
}

pub fn ensure_eject_metric_labels(addr: &str) {
    let addr = addr.to_ascii_lowercase();
    let _ = EJECTIONS_TOTAL.with_label_values(&["success", &addr]);
    let _ = EJECTIONS_TOTAL.with_label_values(&["error", &addr]);
}

pub fn inc_eject_success(addr: &str) {
    let addr = addr.to_ascii_lowercase();
    EJECTIONS_TOTAL.with_label_values(&["success", &addr]).inc();
}
pub fn inc_eject_error(addr: &str) {
    let addr = addr.to_ascii_lowercase();
    EJECTIONS_TOTAL.with_label_values(&["error", &addr]).inc();
}

pub fn inc_l1_event_scanner_restarts() {
    L1_EVENT_SCANNER_RESTARTS_TOTAL.inc();
}

pub fn inc_l2_poll_uncertain() {
    L2_POLL_UNCERTAIN_TOTAL.inc();
}

pub fn set_last_seen_drift_seconds(seconds: u64) {
    let clamped_seconds = i64::try_from(seconds).unwrap_or(i64::MAX);
    LAST_SEEN_DRIFT_SECONDS.set(clamped_seconds);
}

pub fn set_last_block_age_seconds(seconds: u64) {
    let clamped_seconds = i64::try_from(seconds).unwrap_or(i64::MAX);
    LAST_BLOCK_AGE_SECONDS.set(clamped_seconds);
}

pub fn note_reorg(depth: usize, reorg_height: u64) {
    REORG_COUNT_TOTAL.inc();
    let clamped_depth = i64::try_from(depth).unwrap_or(i64::MAX);
    REORG_DEPTH_BLOCKS.set(clamped_depth);
    let clamped_height = i64::try_from(reorg_height).unwrap_or(i64::MAX);
    LAST_REORGED_TO.set(clamped_height);
}

pub fn inc_reorg_skipped() {
    REORG_SKIPPED_TOTAL.inc();
}

pub fn set_preconfer_missing_from_whitelist(addr: Address, missing: bool) {
    let addr = format!("{addr:#x}").to_ascii_lowercase();
    let value = if missing { 1 } else { 0 };
    PRECONFER_MISSING_FROM_WHITELIST.with_label_values(&[&addr]).set(value);
}
