use crate::utils::fixed::Fixed128;
use rust_decimal::Decimal;
use serde::Serialize;
use utoipa::{
    openapi::{SchemaFormat, Type},
    PartialSchema, ToSchema,
};

#[cfg(feature = "utoipa-axum")]
pub use utoipa_axum_feat::*;

use super::tokens::Brc4Ticker;

#[cfg(feature = "utoipa-axum")]
mod utoipa_axum_feat {
    use std::convert::Infallible;

    use axum::{handler::Handler, routing::MethodRouter};
    use utoipa::openapi::{
        path::{Operation, OperationBuilder},
        PathItem, RefOr, Schema,
    };
    use utoipa_axum::router::OpenApiRouter;

    pub enum Method<S> {
        Post(MethodRouter<S, Infallible>),
        Get(MethodRouter<S, Infallible>),
        Put(MethodRouter<S, Infallible>),
        Delete(MethodRouter<S, Infallible>),
    }

    enum MethodType {
        Post,
        Get,
        Put,
        Delete,
    }

    impl<S> Method<S> {
        fn extract(self) -> (MethodType, MethodRouter<S>) {
            match self {
                Method::Post(method_router) => (MethodType::Post, method_router),
                Method::Get(method_router) => (MethodType::Get, method_router),
                Method::Put(method_router) => (MethodType::Put, method_router),
                Method::Delete(method_router) => (MethodType::Delete, method_router),
            }
        }
    }

    pub trait OperationMod {
        fn and_mutate<F>(&mut self, func: F) -> Operation
        where
            F: FnMut(&mut Operation);
        fn and_modify<F>(self, func: F) -> Operation
        where
            F: FnOnce(Operation) -> Operation;
    }

    impl OperationMod for Operation {
        fn and_modify<F>(self, func: F) -> Operation
        where
            F: FnOnce(Operation) -> Operation,
        {
            func(self)
        }

        fn and_mutate<F>(&mut self, mut func: F) -> Operation
        where
            F: FnMut(&mut Operation),
        {
            func(self);
            self.clone()
        }
    }

    pub trait UtoipaAxumExtras<S> {
        fn route_desc_full(self, path: &str, method_router: Method<S>, op_builder: Operation) -> Self;
        fn route_desc(self, path: &str, method_router: Method<S>) -> Self;
        fn schemas(self, schemas: Vec<(String, RefOr<Schema>)>) -> Self;
    }

    impl<S> UtoipaAxumExtras<S> for OpenApiRouter<S>
    where
        S: Send + Sync + Clone + 'static,
    {
        fn route_desc_full(self, path: &str, method_router: Method<S>, op: Operation) -> Self {
            let (typ, method_router) = method_router.extract();
            let (a, mut b) = self.route(path, method_router).split_for_parts();

            let mut item: PathItem = PathItem::default();
            match typ {
                MethodType::Post => item.post = Some(op),
                MethodType::Get => item.get = Some(op),
                MethodType::Put => item.put = Some(op),
                MethodType::Delete => item.delete = Some(op),
            }

            // add or merge current paths to the OpenApi
            if let Some(it) = b.paths.paths.get_mut(path) {
                it.merge_operations(item);
            } else {
                b.paths.paths.insert(path.to_string(), item);
            }

            OpenApiRouter::with_openapi(b).merge(a.into())
        }
        fn route_desc(self, path: &str, method_router: Method<S>) -> Self {
            let op_builder = OperationBuilder::new().build();
            self.route_desc_full(path, method_router, op_builder)
        }
        fn schemas(self, schemas: Vec<(String, RefOr<Schema>)>) -> Self {
            let (a, mut b) = self.split_for_parts();
            let components = b.components.get_or_insert(utoipa::openapi::Components::new());
            components.schemas.extend(schemas);
            OpenApiRouter::with_openapi(b).merge(a.into())
        }
    }

    pub fn post<H, T, S>(handler: H) -> Method<S>
    where
        H: Handler<T, S>,
        T: 'static,
        S: Clone + Send + Sync + 'static,
    {
        Method::Post(axum::routing::post(handler))
    }

    pub fn put<H, T, S>(handler: H) -> Method<S>
    where
        H: Handler<T, S>,
        T: 'static,
        S: Clone + Send + Sync + 'static,
    {
        Method::Put(axum::routing::put(handler))
    }

    pub fn delete<H, T, S>(handler: H) -> Method<S>
    where
        H: Handler<T, S>,
        T: 'static,
        S: Clone + Send + Sync + 'static,
    {
        Method::Delete(axum::routing::delete(handler))
    }

    pub fn get<H, T, S>(handler: H) -> Method<S>
    where
        H: Handler<T, S>,
        T: 'static,
        S: Clone + Send + Sync + 'static,
    {
        Method::Get(axum::routing::get(handler))
    }
}

#[derive(Serialize)]
pub struct SchemedDecimal(pub Decimal);

impl<const PRECISION: u8> From<Fixed128<PRECISION>> for SchemedDecimal {
    fn from(value: Fixed128<PRECISION>) -> Self {
        SchemedDecimal(value.into_decimal())
    }
}

impl From<Decimal> for SchemedDecimal {
    fn from(value: Decimal) -> Self {
        SchemedDecimal(value)
    }
}

impl ToSchema for Brc4Ticker {
    fn name() -> std::borrow::Cow<'static, str> {
        let full_type_name = std::any::type_name::<Self>();
        let type_name_without_generic = full_type_name.split_once("<").map(|(s1, _)| s1).unwrap_or(full_type_name);
        let type_name = type_name_without_generic.rsplit_once("::").map(|(_, tn)| tn).unwrap_or(type_name_without_generic);
        std::borrow::Cow::Borrowed(type_name)
    }
}

impl PartialSchema for Brc4Ticker {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        utoipa::openapi::RefOr::T(utoipa::openapi::Schema::Object(
            utoipa::openapi::ObjectBuilder::new()
                .title(Some("tick".to_string()))
                .schema_type(Type::String)
                .format(Some(SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Float)))
                .description(Some("brc-20 token tick"))
                .default(Some(serde_json::to_value("0").unwrap()))
                .minimum(Some(0))
                .examples(vec![
                    Some(serde_json::to_value("nook").unwrap()),
                    Some(serde_json::to_value("1234").unwrap()),
                    Some(serde_json::to_value("æœ").unwrap()),
                    Some(serde_json::to_value("汉 ").unwrap()),
                    Some(serde_json::to_value("ҐҐ").unwrap()),
                ])
                .build(),
        ))
    }
}
impl ToSchema for SchemedDecimal {
    fn name() -> std::borrow::Cow<'static, str> {
        let full_type_name = std::any::type_name::<Self>();
        let type_name_without_generic = full_type_name.split_once("<").map(|(s1, _)| s1).unwrap_or(full_type_name);
        let type_name = type_name_without_generic.rsplit_once("::").map(|(_, tn)| tn).unwrap_or(type_name_without_generic);
        std::borrow::Cow::Borrowed(type_name)
    }
}

impl PartialSchema for SchemedDecimal {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        utoipa::openapi::RefOr::T(utoipa::openapi::Schema::Object(
            utoipa::openapi::ObjectBuilder::new()
                .title(Some("decimal".to_string()))
                .schema_type(Type::String)
                .format(Some(SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Float)))
                .description(Some("`Decimal` represents a 128 bit representation of a fixed-precision decimal number."))
                .default(Some(serde_json::to_value("0").unwrap()))
                .minimum(Some(0))
                .examples(vec![
                    Some(serde_json::to_value("0").unwrap()),
                    Some(serde_json::to_value("1.0").unwrap()),
                    Some(serde_json::to_value("3.5").unwrap()),
                    Some(serde_json::to_value("13.31").unwrap()),
                ])
                .build(),
        ))
    }
}
impl<const PRECISION: u8> ToSchema for Fixed128<PRECISION> {
    fn name() -> std::borrow::Cow<'static, str> {
        let full_type_name = std::any::type_name::<Self>();
        let type_name_without_generic = full_type_name.split_once("<").map(|(s1, _)| s1).unwrap_or(full_type_name);
        let type_name = type_name_without_generic.rsplit_once("::").map(|(_, tn)| tn).unwrap_or(type_name_without_generic);
        std::borrow::Cow::Borrowed(type_name)
    }
}

impl<const PRECISION: u8> PartialSchema for Fixed128<PRECISION> {
    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
        utoipa::openapi::RefOr::T(utoipa::openapi::Schema::Object(
            utoipa::openapi::ObjectBuilder::new()
                .title(Some("fixed".to_string()))
                .schema_type(Type::String)
                .format(Some(SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Float)))
                .description(Some("128-bit unsigned fixed point number with const generic precision."))
                .default(Some(serde_json::to_value("0").unwrap()))
                .minimum(Some(0))
                .examples(vec![
                    Some(serde_json::to_value("0").unwrap()),
                    Some(serde_json::to_value("1.0").unwrap()),
                    Some(serde_json::to_value("3.5").unwrap()),
                    Some(serde_json::to_value("13.31").unwrap()),
                ])
                .build(),
        ))
    }
}
