use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
use serde_json::Value;

pub mod interning;

pub fn wrap_py_err<E: std::fmt::Display>(e: E) -> PyErr {
    PyRuntimeError::new_err(e.to_string())
}

pub fn parse_uri(uri: &str) -> PyResult<url::Url> {
    match url::Url::parse(uri) {
        Ok(u) => Ok(u),
        Err(e) => {
            if e == url::ParseError::RelativeUrlWithoutBase {
                let path = std::path::Path::new(uri);
                if path.is_absolute() {
                    return url::Url::from_file_path(path).map_err(|_| {
                        PyRuntimeError::new_err(format!("Invalid file path: {}", uri))
                    });
                } else {
                    return Err(PyRuntimeError::new_err(format!(
                        "Relative path without scheme: {}",
                        uri
                    )));
                }
            }
            Err(PyRuntimeError::new_err(e.to_string()))
        }
    }
}

pub fn generate_entry_id(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
    let mut buf = [0u8; 32];
    uuid::Uuid::new_v4().simple().encode_lower(&mut buf);
    match std::str::from_utf8(&buf) {
        Ok(s) => Ok(PyString::new(py, s).into_any()),
        Err(e) => Err(wrap_py_err(e)),
    }
}

pub fn wrap_in_envelope<'py>(
    py: Python<'py>,
    keys: &interning::InternedKeys,
    raw_data: Bound<'py, PyAny>,
    status: Bound<'py, PyAny>,
    position: usize,
    generate_ids: bool,
) -> PyResult<Bound<'py, PyAny>> {
    let envelope = PyDict::new(py);

    let id = if generate_ids {
        generate_entry_id(py)?
    } else {
        py.None().into_bound(py)
    };

    envelope.set_item(keys.get_id(py), id)?;
    envelope.set_item(keys.get_status(py), status)?;
    envelope.set_item(keys.get_raw_data(py), raw_data)?;
    envelope.set_item(keys.get_metadata(py), PyDict::new(py))?;
    envelope.set_item(keys.get_position(py), position)?;
    envelope.set_item(keys.get_errors(py), PyList::empty(py))?;

    Ok(envelope.into_any())
}

pub fn serde_to_py<'py>(py: Python<'py>, value: Value) -> PyResult<Bound<'py, PyAny>> {
    match value {
        Value::Null => Ok(py.None().into_bound(py)),
        Value::Bool(b) => Ok(PyBool::new(py, b).as_any().clone()),
        Value::Number(n) => {
            if let Some(i) = n.as_i64() {
                Ok(PyInt::new(py, i).as_any().clone())
            } else if let Some(f) = n.as_f64() {
                Ok(PyFloat::new(py, f).as_any().clone())
            } else {
                Ok(PyString::new(py, &n.to_string()).as_any().clone())
            }
        }
        Value::String(s) => Ok(PyString::new(py, &s).as_any().clone()),
        Value::Array(arr) => {
            let elements: Vec<_> = arr
                .into_iter()
                .map(|v| serde_to_py(py, v))
                .collect::<PyResult<Vec<_>>>()?;
            let list = PyList::new(py, elements)?;
            Ok(list.as_any().clone())
        }
        Value::Object(obj) => {
            let dict = PyDict::new(py);
            for (k, v) in obj {
                dict.set_item(k, serde_to_py(py, v)?)?;
            }
            Ok(dict.as_any().clone())
        }
    }
}

pub struct PySerializable<'a>(pub Bound<'a, PyAny>);

impl<'a> Serialize for PySerializable<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let obj = &self.0;
        if obj.is_none() {
            serializer.serialize_none()
        } else if let Ok(b) = obj.cast::<PyBool>() {
            serializer.serialize_bool(b.is_true())
        } else if let Ok(i) = obj.cast::<PyInt>() {
            if let Ok(val) = i.extract::<i64>() {
                serializer.serialize_i64(val)
            } else {
                serializer.serialize_str(&i.to_string())
            }
        } else if let Ok(f) = obj.cast::<PyFloat>() {
            if let Ok(val) = f.extract::<f64>() {
                serializer.serialize_f64(val)
            } else {
                serializer.serialize_none()
            }
        } else if let Ok(s) = obj.cast::<PyString>() {
            if let Ok(val) = s.to_str() {
                serializer.serialize_str(val)
            } else {
                serializer.serialize_str(&s.to_string())
            }
        } else if let Ok(l) = obj.cast::<PyList>() {
            let mut seq = serializer.serialize_seq(Some(l.len()))?;
            for item in l.iter() {
                seq.serialize_element(&PySerializable(item))?;
            }
            seq.end()
        } else if let Ok(d) = obj.cast::<PyDict>() {
            let mut map = serializer.serialize_map(Some(d.len()))?;
            for (k, v) in d.iter() {
                if let Ok(ks) = k.cast::<PyString>() {
                    if let Ok(ks_val) = ks.to_str() {
                        map.serialize_entry(ks_val, &PySerializable(v))?;
                    } else {
                        map.serialize_entry(&ks.to_string(), &PySerializable(v))?;
                    }
                } else {
                    map.serialize_entry(&k.to_string(), &PySerializable(v))?;
                }
            }
            map.end()
        } else if let Ok(s) = obj.extract::<&str>() {
            serializer.serialize_str(s)
        } else {
            let s = obj.to_string();
            serializer.serialize_str(&s)
        }
    }
}
