use std::sync::Arc;
use std::time::Instant;

use chrono::Utc;
use tonic::{Request, Response, Status};

use crate::ingestion::IngestionMetrics;
use crate::query::{QueryMetrics, QueryResult as InstantQueryData, QueryService, RangeQueryResult};
use crate::storage::{Storage, StorageStats};
use nightlight_api::nightlight::admin_server::Admin;
use nightlight_api::nightlight::metric_query_server::MetricQuery;
use nightlight_api::nightlight::{
    BuildInfoRequest, BuildInfoResponse, ComponentHealth, HealthRequest, HealthResponse,
    LabelValuesRequest, LabelValuesResponse, QueryData, QueryResponse, QueryResult, SamplePair,
    SeriesLabels, SeriesQueryRequest, SeriesQueryResponse, StatsRequest, StatsResponse,
};
use nightlight_types::Error;

#[derive(Clone)]
pub struct MetricQueryServiceImpl {
    query: QueryService,
}

#[derive(Clone)]
pub struct AdminServiceImpl {
    storage: Arc<Storage>,
    ingestion_metrics: Arc<IngestionMetrics>,
    query_metrics: Arc<QueryMetrics>,
    started_at: Instant,
}

impl MetricQueryServiceImpl {
    pub fn new(query: QueryService) -> Self {
        Self { query }
    }
}

impl AdminServiceImpl {
    pub fn new(
        storage: Arc<Storage>,
        ingestion_metrics: Arc<IngestionMetrics>,
        query_metrics: Arc<QueryMetrics>,
    ) -> Self {
        Self {
            storage,
            ingestion_metrics,
            query_metrics,
            started_at: Instant::now(),
        }
    }
}

#[tonic::async_trait]
impl MetricQuery for MetricQueryServiceImpl {
    async fn instant_query(
        &self,
        request: Request<nightlight_api::nightlight::InstantQueryRequest>,
    ) -> Result<Response<QueryResponse>, Status> {
        let request = request.into_inner();
        let time = if request.time == 0 {
            Utc::now().timestamp_millis()
        } else {
            request.time
        };

        let response = match self.query.execute_instant_query(&request.query, time).await {
            Ok(result) => QueryResponse {
                status: "success".to_string(),
                data: Some(instant_query_data_to_proto(result)),
                error: String::new(),
                error_type: String::new(),
                warnings: Vec::new(),
            },
            Err(error) => QueryResponse {
                status: "error".to_string(),
                data: None,
                error: error.to_string(),
                error_type: query_error_type(&error).to_string(),
                warnings: Vec::new(),
            },
        };

        Ok(Response::new(response))
    }

    async fn range_query(
        &self,
        request: Request<nightlight_api::nightlight::RangeQueryRequest>,
    ) -> Result<Response<QueryResponse>, Status> {
        let request = request.into_inner();

        let response = match self
            .query
            .execute_range_query(&request.query, request.start, request.end, request.step)
            .await
        {
            Ok(result) => QueryResponse {
                status: "success".to_string(),
                data: Some(range_query_data_to_proto(result)),
                error: String::new(),
                error_type: String::new(),
                warnings: Vec::new(),
            },
            Err(error) => QueryResponse {
                status: "error".to_string(),
                data: None,
                error: error.to_string(),
                error_type: query_error_type(&error).to_string(),
                warnings: Vec::new(),
            },
        };

        Ok(Response::new(response))
    }

    async fn series_query(
        &self,
        request: Request<SeriesQueryRequest>,
    ) -> Result<Response<SeriesQueryResponse>, Status> {
        let request = request.into_inner();
        let response = match self
            .query
            .series_metadata(
                &request.r#match,
                optional_millis(request.start),
                optional_millis(request.end),
            )
            .await
        {
            Ok(series) => SeriesQueryResponse {
                status: "success".to_string(),
                data: series
                    .into_iter()
                    .map(|labels| SeriesLabels { labels })
                    .collect(),
                error: String::new(),
            },
            Err(error) => SeriesQueryResponse {
                status: "error".to_string(),
                data: Vec::new(),
                error: error.to_string(),
            },
        };

        Ok(Response::new(response))
    }

    async fn label_values_query(
        &self,
        request: Request<LabelValuesRequest>,
    ) -> Result<Response<LabelValuesResponse>, Status> {
        let request = request.into_inner();
        let response = match self
            .query
            .label_values_for_matchers(
                &request.label_name,
                &request.r#match,
                optional_millis(request.start),
                optional_millis(request.end),
            )
            .await
        {
            Ok(values) => LabelValuesResponse {
                status: "success".to_string(),
                data: values,
                error: String::new(),
            },
            Err(error) => LabelValuesResponse {
                status: "error".to_string(),
                data: Vec::new(),
                error: error.to_string(),
            },
        };

        Ok(Response::new(response))
    }
}

#[tonic::async_trait]
impl Admin for AdminServiceImpl {
    async fn health(
        &self,
        _request: Request<HealthRequest>,
    ) -> Result<Response<HealthResponse>, Status> {
        let storage_result = self.storage.stats().await;
        let status = if storage_result.is_ok() {
            "ok"
        } else {
            "degraded"
        };
        let storage_message = match &storage_result {
            Ok(_) => "storage ready".to_string(),
            Err(error) => error.to_string(),
        };

        Ok(Response::new(HealthResponse {
            status: status.to_string(),
            message: "nightlight ready".to_string(),
            components: vec![
                ComponentHealth {
                    name: "storage".to_string(),
                    status: status.to_string(),
                    message: storage_message,
                },
                ComponentHealth {
                    name: "ingestion".to_string(),
                    status: "ok".to_string(),
                    message: "remote_write endpoint ready".to_string(),
                },
                ComponentHealth {
                    name: "query_engine".to_string(),
                    status: "ok".to_string(),
                    message: "http and grpc query paths ready".to_string(),
                },
            ],
        }))
    }

    async fn stats(
        &self,
        _request: Request<StatsRequest>,
    ) -> Result<Response<StatsResponse>, Status> {
        let storage = self
            .storage
            .stats()
            .await
            .map_err(|error| Status::internal(error.to_string()))?;
        let ingestion = self.ingestion_metrics.snapshot();
        let query = self.query_metrics.snapshot();

        Ok(Response::new(StatsResponse {
            storage: Some(storage_stats_to_proto(storage)),
            ingestion: Some(nightlight_api::nightlight::IngestionStats {
                samples_ingested_total: ingestion.samples_ingested_total,
                write_requests_total: ingestion.write_requests_total,
                write_requests_failed: ingestion.write_requests_failed,
                samples_per_second: ingestion.samples_per_second,
                buffer_samples: ingestion.buffer_samples,
            }),
            query: Some(nightlight_api::nightlight::QueryStats {
                queries_total: query.queries_total,
                queries_failed: query.queries_failed,
                queries_active: query.queries_active,
                query_duration_p50: query.query_duration_p50,
                query_duration_p95: query.query_duration_p95,
                query_duration_p99: query.query_duration_p99,
            }),
            uptime_seconds: self.started_at.elapsed().as_secs(),
        }))
    }

    async fn build_info(
        &self,
        _request: Request<BuildInfoRequest>,
    ) -> Result<Response<BuildInfoResponse>, Status> {
        Ok(Response::new(BuildInfoResponse {
            version: env!("CARGO_PKG_VERSION").to_string(),
            commit: option_env!("GIT_COMMIT").unwrap_or("unknown").to_string(),
            build_time: option_env!("BUILD_TIME").unwrap_or("unknown").to_string(),
            rust_version: option_env!("RUSTC_VERSION")
                .unwrap_or("unknown")
                .to_string(),
            target: format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS),
        }))
    }
}

fn optional_millis(value: i64) -> Option<i64> {
    if value == 0 {
        None
    } else {
        Some(value)
    }
}

fn query_error_type(error: &Error) -> &'static str {
    match error {
        Error::InvalidMetric(_) | Error::InvalidLabel(_) | Error::InvalidTimeRange(_) => "bad_data",
        Error::Timeout(_) => "timeout",
        _ => "execution",
    }
}

fn instant_query_data_to_proto(result: InstantQueryData) -> QueryData {
    QueryData {
        result_type: result.result_type,
        result: result
            .result
            .into_iter()
            .map(|series| QueryResult {
                metric: series.metric,
                values: Vec::new(),
                value: series.value.map(sample_pair_from_tuple),
            })
            .collect(),
    }
}

fn range_query_data_to_proto(result: RangeQueryResult) -> QueryData {
    QueryData {
        result_type: result.result_type,
        result: result
            .result
            .into_iter()
            .map(|series| QueryResult {
                metric: series.metric,
                values: series
                    .values
                    .into_iter()
                    .map(sample_pair_from_tuple)
                    .collect(),
                value: None,
            })
            .collect(),
    }
}

fn sample_pair_from_tuple((timestamp, value): (i64, f64)) -> SamplePair {
    SamplePair { timestamp, value }
}

fn storage_stats_to_proto(stats: StorageStats) -> nightlight_api::nightlight::StorageStats {
    nightlight_api::nightlight::StorageStats {
        active_series: stats.active_series,
        total_samples: stats.total_samples,
        blocks_count: stats.blocks_count,
        head_samples: stats.head_samples,
        disk_bytes_used: stats.disk_bytes_used,
        oldest_sample_time: stats.oldest_sample_time,
        newest_sample_time: stats.newest_sample_time,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ingestion::IngestionService;
    use crate::storage::Storage;
    use nightlight_api::nightlight::{InstantQueryRequest, LabelValuesRequest, SeriesQueryRequest};
    use nightlight_api::prometheus::{Label, Sample, TimeSeries, WriteRequest};

    #[tokio::test]
    async fn instant_query_grpc_returns_metric_data() {
        let dir = tempfile::tempdir().unwrap();
        let storage = Arc::new(Storage::new(dir.path().to_str().unwrap()).unwrap());
        let ingestion = IngestionService::new(Arc::clone(&storage));
        ingestion
            .process_write_request(WriteRequest {
                timeseries: vec![TimeSeries {
                    labels: vec![
                        Label {
                            name: "__name__".to_string(),
                            value: "grpc_metric".to_string(),
                        },
                        Label {
                            name: "job".to_string(),
                            value: "nightlight".to_string(),
                        },
                    ],
                    samples: vec![Sample {
                        value: 12.5,
                        timestamp: 1_000,
                    }],
                }],
            })
            .await
            .unwrap();

        let service = MetricQueryServiceImpl::new(QueryService::from_storage(storage.queryable()));
        let response = service
            .instant_query(Request::new(InstantQueryRequest {
                query: "grpc_metric{job=\"nightlight\"}".to_string(),
                time: 2_000,
                timeout: 0,
            }))
            .await
            .unwrap()
            .into_inner();

        assert_eq!(response.status, "success");
        let data = response.data.unwrap();
        assert_eq!(data.result.len(), 1);
        assert_eq!(
            data.result[0].metric.get("__name__").map(String::as_str),
            Some("grpc_metric")
        );
        assert_eq!(
            data.result[0].value.as_ref().map(|value| value.value),
            Some(12.5)
        );
    }

    #[tokio::test]
    async fn metadata_queries_grpc_filter_series() {
        let dir = tempfile::tempdir().unwrap();
        let storage = Arc::new(Storage::new(dir.path().to_str().unwrap()).unwrap());
        let ingestion = IngestionService::new(Arc::clone(&storage));
        ingestion
            .process_write_request(WriteRequest {
                timeseries: vec![
                    TimeSeries {
                        labels: vec![
                            Label {
                                name: "__name__".to_string(),
                                value: "grpc_metric".to_string(),
                            },
                            Label {
                                name: "job".to_string(),
                                value: "api".to_string(),
                            },
                        ],
                        samples: vec![Sample {
                            value: 1.0,
                            timestamp: 1_000,
                        }],
                    },
                    TimeSeries {
                        labels: vec![
                            Label {
                                name: "__name__".to_string(),
                                value: "grpc_metric".to_string(),
                            },
                            Label {
                                name: "job".to_string(),
                                value: "worker".to_string(),
                            },
                        ],
                        samples: vec![Sample {
                            value: 2.0,
                            timestamp: 2_000,
                        }],
                    },
                ],
            })
            .await
            .unwrap();

        let service = MetricQueryServiceImpl::new(QueryService::from_storage(storage.queryable()));
        let series = service
            .series_query(Request::new(SeriesQueryRequest {
                r#match: vec!["job=api".to_string()],
                start: 0,
                end: 0,
            }))
            .await
            .unwrap()
            .into_inner();
        assert_eq!(series.status, "success");
        assert_eq!(series.data.len(), 1);

        let label_values = service
            .label_values_query(Request::new(LabelValuesRequest {
                label_name: "job".to_string(),
                r#match: vec!["__name__=grpc_metric".to_string()],
                start: 0,
                end: 0,
            }))
            .await
            .unwrap()
            .into_inner();
        assert_eq!(label_values.status, "success");
        assert_eq!(
            label_values.data,
            vec!["api".to_string(), "worker".to_string()]
        );
    }

    #[tokio::test]
    async fn admin_stats_report_ingestion_and_query_counters() {
        let dir = tempfile::tempdir().unwrap();
        let storage = Arc::new(Storage::new(dir.path().to_str().unwrap()).unwrap());
        let ingestion = IngestionService::new(Arc::clone(&storage));
        ingestion
            .process_write_request(WriteRequest {
                timeseries: vec![TimeSeries {
                    labels: vec![Label {
                        name: "__name__".to_string(),
                        value: "admin_metric".to_string(),
                    }],
                    samples: vec![Sample {
                        value: 3.0,
                        timestamp: 1_000,
                    }],
                }],
            })
            .await
            .unwrap();

        let query = QueryService::from_storage(storage.queryable());
        query
            .execute_instant_query("admin_metric", 2_000)
            .await
            .unwrap();

        let admin =
            AdminServiceImpl::new(Arc::clone(&storage), ingestion.metrics(), query.metrics());
        let stats = admin
            .stats(Request::new(StatsRequest {}))
            .await
            .unwrap()
            .into_inner();

        assert_eq!(
            stats.storage.as_ref().map(|value| value.total_samples),
            Some(1)
        );
        assert_eq!(
            stats
                .ingestion
                .as_ref()
                .map(|value| value.samples_ingested_total),
            Some(1)
        );
        assert_eq!(
            stats.query.as_ref().map(|value| value.queries_total),
            Some(1)
        );
    }
}
