use crate::service::MagnetarService;
use crate::util::serialize_as_urlenc;
use crate::web::{ApiError, IntoErrorCode};
use axum::extract::rejection::QueryRejection;
use axum::extract::{FromRequestParts, OriginalUri, Query};
use axum::http::header::InvalidHeaderValue;
use axum::http::request::Parts;
use axum::http::{HeaderValue, StatusCode, Uri};
use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts};
use axum::RequestPartsExt;
use either::Either;
use itertools::Itertools;
use magnetar_core::web_model::rel::{RelNext, RelPrev};
use magnetar_model::sea_orm::prelude::async_trait::async_trait;
use magnetar_sdk::types::{PaginationShape, SpanFilter};
use magnetar_sdk::util_types::U64Range;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use strum::IntoStaticStr;
use thiserror::Error;
use tracing::error;

#[derive(Debug)]
pub struct Pagination {
    base_uri: Uri,
    pub current: SpanFilter,
    pub prev: Option<SpanFilter>,
    pub next: Option<SpanFilter>,
    pub limit: U64Range<10, 100>,
    query_rest: HashMap<String, String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct PaginationQuery {
    #[serde(flatten)]
    pagination: PaginationShape,
    #[serde(flatten)]
    query_rest: HashMap<String, String>,
}

#[derive(Debug, Error, IntoStaticStr)]
pub enum PaginationBuilderError {
    #[error("Query rejection: {0}")]
    QueryRejection(#[from] QueryRejection),
    #[error("HTTP error: {0}")]
    HttpError(#[from] axum::http::Error),
    #[error("Value of out of range error")]
    OutOfRange,
    #[error("Invalid header value")]
    InvalidHeaderValue(#[from] InvalidHeaderValue),
    #[error("Query string serialization error: {0}")]
    SerializationErrorQuery(#[from] serde_urlencoded::ser::Error),
    #[error("Query string serialization error: {0}")]
    SerializationErrorJson(#[from] serde_json::Error),
}

impl From<PaginationBuilderError> for ApiError {
    fn from(err: PaginationBuilderError) -> Self {
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            code: err.error_code(),
            message: if cfg!(debug_assertions) {
                format!("Pagination builder error: {}", err)
            } else {
                "Pagination builder error".to_string()
            },
        }
    }
}

impl IntoResponse for PaginationBuilderError {
    fn into_response(self) -> Response {
        ApiError::from(self).into_response()
    }
}

#[async_trait]
impl FromRequestParts<Arc<MagnetarService>> for Pagination {
    type Rejection = PaginationBuilderError;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &Arc<MagnetarService>,
    ) -> Result<Self, Self::Rejection> {
        let OriginalUri(original_uri) = parts.extract::<OriginalUri>().await.unwrap();

        let base_uri = Uri::builder()
            .scheme(state.config.networking.protocol.as_ref())
            .authority(state.config.networking.host.as_str())
            .path_and_query(original_uri.path())
            .build()?;

        let Query(PaginationQuery {
            pagination,
            query_rest,
        }) = parts.extract::<Query<PaginationQuery>>().await?;

        Ok(Pagination {
            base_uri,
            prev: None,
            next: None,
            current: pagination.pagination,
            limit: pagination.limit,
            query_rest,
        })
    }
}

impl IntoResponseParts for Pagination {
    type Error = PaginationBuilderError;

    fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
        let wrap = |uri: Uri, query: String, rel: Either<RelPrev, RelNext>| {
            format!(
                "<{}?{}>; rel=\"{}\"",
                uri,
                query,
                rel.as_ref()
                    .map_either(RelPrev::as_ref, RelNext::as_ref)
                    .into_inner()
            )
        };

        let url_prev = if let Some(prev) = self.prev {
            let query_prev = serialize_as_urlenc(&serde_json::to_value(PaginationQuery {
                pagination: PaginationShape {
                    pagination: prev,
                    limit: self.limit,
                },
                query_rest: self.query_rest.clone(),
            })?);

            Some(wrap(
                self.base_uri.clone(),
                query_prev,
                Either::Left(RelPrev),
            ))
        } else {
            None
        };

        let url_next = if let Some(next) = self.next {
            let query_next = serialize_as_urlenc(&serde_json::to_value(PaginationQuery {
                pagination: PaginationShape {
                    pagination: next,
                    limit: self.limit,
                },
                query_rest: self.query_rest,
            })?);

            Some(wrap(self.base_uri, query_next, Either::Right(RelNext)))
        } else {
            None
        };

        let parts = [url_prev, url_next].iter().flatten().join(", ");
        if !parts.is_empty() {
            res.headers_mut()
                .insert(axum::http::header::LINK, HeaderValue::from_str(&parts)?);
        }
        Ok(res)
    }
}
