use crate::utils::fixed::{Fixed128, Fixed256};
use rust_decimal::Decimal;
use serde::{Deserialize, 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, UtoipaMethodRouter};

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

    pub 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 {
        /// mutate provided operation then return cloned version
        fn and_mutate<F>(&mut self, func: F) -> Operation
        where
            F: FnMut(&mut Operation);
        /// clone operation then modify it
        fn and_modify<F>(&self, func: F) -> Operation
        where
            F: FnMut(&mut Operation);
        /// take owned operation then modify it
        /// intended to use with `.and_mutate()` or `.clone()`
        fn then_modify<F>(self, func: F) -> Operation
        where
            F: FnMut(&mut Operation);
        /// set description for operation
        fn desc(&mut self, desc: impl Into<String>);
    }

    impl OperationMod for Operation {
        fn and_modify<F>(&self, mut func: F) -> Operation
        where
            F: FnMut(&mut Operation),
        {
            let mut s = self.clone();
            func(&mut s);
            s
        }

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

        fn desc(&mut self, desc: impl Into<String>) {
            self.description = Some(desc.into())
        }

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

    pub trait UtoipaAxumExtras<S> {
        fn route_desc_full(self, path: &str, method_router: Method<S>, op: Operation) -> Self;
        fn route_desc(self, path: &str, method_router: Method<S>) -> Self;
        fn schemas(self, schemas: Vec<(String, RefOr<Schema>)>) -> Self;
        fn routes2_lol(self, a: UtoipaMethodRouter<S>, new_path: &str) -> Self;
        fn routes2_lol_with_op_merge(self, a: UtoipaMethodRouter<S>, new_path: &str, method_type: MethodType, op: Operation) -> 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())
        }

        /// taked from here `utoipa_axum::router::OpenApiRouter::routes`
        fn routes2_lol(self, (schemas, mut paths, method_router): UtoipaMethodRouter<S>, new_path: &str) -> Self {
            let (a, mut b) = self.split_for_parts();

            let a = if paths.paths.len() == 1 {
                let first_entry = &paths.paths.first_entry();
                let path = first_entry.as_ref().map(|path| path.key());
                let Some(path) = path else {
                    unreachable!("Whoopsie, I thought there was one Path entry");
                };
                let path = if path.is_empty() { "/" } else { new_path };

                a.route(path, method_router)
            } else {
                panic!("Nope, just one route here, and go fuck yourself otherwise");
            };

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

            let components = b.components.get_or_insert(utoipa::openapi::Components::new());
            components.schemas.extend(schemas);

            OpenApiRouter::with_openapi(b).merge(a.into())
        }

        fn routes2_lol_with_op_merge(self, (schemas, mut paths, method_router): UtoipaMethodRouter<S>, new_path: &str, method_type: MethodType, op: Operation) -> Self {
            let (a, mut b) = self.split_for_parts();

            let a = if paths.paths.len() == 1 {
                let first_entry = &paths.paths.first_entry();
                let path = first_entry.as_ref().map(|path| path.key());
                let Some(path) = path else {
                    unreachable!("Whoopsie, I thought there was one Path entry");
                };
                let path = if path.is_empty() { "/" } else { new_path };

                a.route(path, method_router)
            } else {
                panic!("Nope, just one route here, and go fuck yourself otherwise");
            };

            // add or merge current paths to the OpenApi
            for (_, mut item) in paths.paths {
                let typ;
                let method = match method_type {
                    MethodType::Post => {
                        typ = "post";
                        &mut item.post
                    }
                    MethodType::Get => {
                        typ = "get";
                        &mut item.get
                    }
                    MethodType::Put => {
                        typ = "put";
                        &mut item.put
                    }
                    MethodType::Delete => {
                        typ = "delete";
                        &mut item.delete
                    }
                };

                if let Some(method) = method {
                    method.operation_id = Some((new_path.trim_start_matches('/').replace('/', "-").replace(['}', '{'], "") + typ).to_string());
                    if let Some(ref tags) = op.tags {
                        method.tags = Some(tags.clone());
                    }
                    if let Some(ref summary) = op.summary {
                        method.summary = Some(summary.clone());
                    }
                    if let Some(ref description) = op.description {
                        method.description = Some(description.clone());
                    }
                    if let Some(ref operation_id) = op.operation_id {
                        method.operation_id = Some(operation_id.clone());
                    }
                    if let Some(ref external_docs) = op.external_docs {
                        method.external_docs = Some(external_docs.clone());
                    }
                    if let Some(ref parameters) = op.parameters {
                        method.parameters = Some(parameters.clone());
                    }
                    if let Some(ref request_body) = op.request_body {
                        method.request_body = Some(request_body.clone());
                    }
                    if let Some(ref callbacks) = op.callbacks {
                        method.callbacks = Some(callbacks.clone());
                    }
                    if let Some(ref deprecated) = op.deprecated {
                        method.deprecated = Some(deprecated.clone());
                    }
                    if let Some(ref security) = op.security {
                        method.security = Some(security.clone());
                    }
                    if let Some(ref servers) = op.servers {
                        method.servers = Some(servers.clone());
                    }
                    if let Some(ref extensions) = op.extensions {
                        method.extensions = Some(extensions.clone());
                    }
                } else {
                    panic!("metod not found");
                }

                if let Some(it) = b.paths.paths.get_mut(new_path) {
                    it.merge_operations(item);
                } else {
                    b.paths.paths.insert(new_path.to_string(), item);
                }
            }

            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, Deserialize, Clone, Copy)]
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(),
        ))
    }
}
impl<const PRECISION: u8> ToSchema for Fixed256<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 Fixed256<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("256-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(),
        ))
    }
}
