use std::borrow::Cow;
use std::sync::Arc;

use pyo3::intern;
use pyo3::prelude::*;
use pyo3::pybacked::PyBackedStr;
use pyo3::types::{PyDict, PyList};

use crate::definitions::DefinitionsBuilder;
use crate::definitions::{DefinitionRef, RecursionSafeCache};

use crate::serializers::SerializationState;
use crate::tools::SchemaDict;

use super::{BuildSerializer, CombinedSerializer, TypeSerializer, py_err_se_err};

#[derive(Debug)]
pub struct DefinitionsSerializerBuilder;

impl BuildSerializer for DefinitionsSerializerBuilder {
    const EXPECTED_TYPE: &'static str = "definitions";

    fn build(
        schema: &Bound<'_, PyDict>,
        config: Option<&Bound<'_, PyDict>>,
        definitions: &mut DefinitionsBuilder<Arc<CombinedSerializer>>,
    ) -> PyResult<Arc<CombinedSerializer>> {
        let py = schema.py();

        let schema_definitions: Bound<'_, PyList> = schema.get_as_req(intern!(py, "definitions"))?;

        for schema_definition in schema_definitions {
            let schema = schema_definition.cast()?;
            let reference = schema.get_as_req::<String>(intern!(py, "ref"))?;
            let serializer = CombinedSerializer::build(schema, config, definitions)?;
            definitions.add_definition(reference, serializer)?;
        }

        let inner_schema = schema.get_as_req(intern!(py, "schema"))?;
        CombinedSerializer::build(&inner_schema, config, definitions)
    }
}

pub struct DefinitionRefSerializer {
    definition: DefinitionRef<Arc<CombinedSerializer>>,
    retry_with_lax_check: RecursionSafeCache<bool>,
}

impl std::fmt::Debug for DefinitionRefSerializer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DefinitionRefSerializer")
            .field("definition", &self.definition)
            .field("retry_with_lax_check", &self.retry_with_lax_check())
            .finish()
    }
}

impl BuildSerializer for DefinitionRefSerializer {
    const EXPECTED_TYPE: &'static str = "definition-ref";

    fn build(
        schema: &Bound<'_, PyDict>,
        _config: Option<&Bound<'_, PyDict>>,
        definitions: &mut DefinitionsBuilder<Arc<CombinedSerializer>>,
    ) -> PyResult<Arc<CombinedSerializer>> {
        let schema_ref: PyBackedStr = schema.get_as_req(intern!(schema.py(), "schema_ref"))?;
        let definition = definitions.get_definition(&schema_ref);
        Ok(CombinedSerializer::Recursive(Self {
            definition,
            retry_with_lax_check: RecursionSafeCache::new(),
        })
        .into())
    }
}

impl_py_gc_traverse!(DefinitionRefSerializer {});

impl TypeSerializer for DefinitionRefSerializer {
    fn to_python<'py>(&self, value: &Bound<'py, PyAny>, state: &mut SerializationState<'py>) -> PyResult<Py<PyAny>> {
        self.definition.read(|comb_serializer| {
            let comb_serializer = comb_serializer.unwrap();
            let mut guard = state.recursion_guard(value, self.definition.id())?;
            comb_serializer.to_python_no_infer(value, guard.state())
        })
    }

    fn json_key<'a, 'py>(
        &self,
        key: &'a Bound<'py, PyAny>,
        state: &mut SerializationState<'py>,
    ) -> PyResult<Cow<'a, str>> {
        self.definition.read(|s| s.unwrap().json_key_no_infer(key, state))
    }

    fn serde_serialize<'py, S: serde::ser::Serializer>(
        &self,
        value: &Bound<'py, PyAny>,
        serializer: S,
        state: &mut SerializationState<'py>,
    ) -> Result<S::Ok, S::Error> {
        self.definition.read(|comb_serializer| {
            let comb_serializer = comb_serializer.unwrap();
            let mut guard = state
                .recursion_guard(value, self.definition.id())
                .map_err(py_err_se_err)?;
            comb_serializer.serde_serialize_no_infer(value, serializer, guard.state())
        })
    }

    fn get_name(&self) -> &str {
        Self::EXPECTED_TYPE
    }

    fn retry_with_lax_check(&self) -> bool {
        *self
            .retry_with_lax_check
            .get_or_init(|| self.definition.read(|s| s.unwrap().retry_with_lax_check()), &false)
    }
}
