use std::borrow::Cow;
use std::cmp::Ordering;
use std::convert::Infallible;
use std::ops::Rem;
use std::str::FromStr;

use jiter::{JsonArray, JsonValue, PartialMode, StringCacheMode};
use num_bigint::BigInt;

use pyo3::exceptions::PyTypeError;
use pyo3::ffi;
use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::PyFunction;
use pyo3::types::{PyBytes, PyComplex, PyFloat, PyFrozenSet, PyIterator, PyMapping, PySet, PyString};

use pyo3::IntoPyObjectExt;
use serde::{Serialize, Serializer, ser::Error};

use crate::errors::{
    ErrorType, ErrorTypeDefaults, InputValue, ToErrorValue, ValError, ValLineError, ValResult, py_err_string,
};
use crate::py_gc::PyGcTraverse;
use crate::tools::new_py_string;
use crate::validators::{CombinedValidator, Exactness, ValidationState, Validator};

use super::{BorrowInput, Input, py_error_on_minusone};

pub struct ValidationMatch<T>(T, Exactness);

impl<T> ValidationMatch<T> {
    pub const fn new(value: T, exactness: Exactness) -> Self {
        Self(value, exactness)
    }

    pub const fn exact(value: T) -> Self {
        Self(value, Exactness::Exact)
    }

    pub const fn strict(value: T) -> Self {
        Self(value, Exactness::Strict)
    }

    pub const fn lax(value: T) -> Self {
        Self(value, Exactness::Lax)
    }

    pub fn require_exact(self) -> Option<T> {
        (self.1 == Exactness::Exact).then_some(self.0)
    }

    pub fn unpack(self, state: &mut ValidationState) -> T {
        state.floor_exactness(self.1);
        self.0
    }

    pub fn into_inner(self) -> T {
        self.0
    }
}

pub struct MaxLengthCheck<'a, INPUT: ?Sized> {
    current_length: usize,
    max_length: Option<usize>,
    field_type: &'a str,
    input: &'a INPUT,
    actual_length: Option<usize>,
}

impl<'a, INPUT: ?Sized> MaxLengthCheck<'a, INPUT> {
    pub(crate) fn new(
        max_length: Option<usize>,
        field_type: &'a str,
        input: &'a INPUT,
        actual_length: Option<usize>,
    ) -> Self {
        Self {
            current_length: 0,
            max_length,
            field_type,
            input,
            actual_length,
        }
    }
}

impl<'py, INPUT: Input<'py> + ?Sized> MaxLengthCheck<'_, INPUT> {
    fn incr(&mut self) -> ValResult<()> {
        if let Some(max_length) = self.max_length {
            self.current_length += 1;
            if self.current_length > max_length {
                return Err(ValError::new_custom_input(
                    ErrorType::TooLong {
                        field_type: self.field_type.to_string(),
                        max_length,
                        actual_length: self.actual_length,
                        context: None,
                    },
                    self.input.to_error_value(),
                ));
            }
        }
        Ok(())
    }
}

macro_rules! any_next_error {
    ($py:expr, $err:ident, $input:expr, $index:ident) => {
        ValError::new_with_loc(
            ErrorType::IterationError {
                error: py_err_string($py, $err),
                context: None,
            },
            $input,
            $index,
        )
    };
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn validate_iter_to_vec<'py>(
    py: Python<'py>,
    iter: impl Iterator<Item = PyResult<impl BorrowInput<'py>>>,
    capacity: usize,
    mut max_length_check: MaxLengthCheck<'_, impl Input<'py> + ?Sized>,
    validator: &CombinedValidator,
    state: &mut ValidationState<'_, 'py>,
    fail_fast: bool,
) -> ValResult<Vec<Py<PyAny>>> {
    let mut output: Vec<Py<PyAny>> = Vec::with_capacity(capacity);
    let mut errors: Vec<ValLineError> = Vec::new();
    let allow_partial = state.allow_partial;

    for (index, is_last_partial, item_result) in state.enumerate_last_partial(iter) {
        state.allow_partial = match is_last_partial {
            true => allow_partial,
            false => PartialMode::Off,
        };
        let item = item_result.map_err(|e| any_next_error!(py, e, max_length_check.input, index))?;
        match validator.validate(py, item.borrow_input(), state) {
            Ok(item) => {
                max_length_check.incr()?;
                output.push(item);
            }
            Err(ValError::LineErrors(line_errors)) => {
                max_length_check.incr()?;
                if !is_last_partial {
                    errors.extend(line_errors.into_iter().map(|err| err.with_outer_location(index)));
                    if fail_fast {
                        return Err(ValError::LineErrors(errors));
                    }
                }
            }
            Err(ValError::Omit) => (),
            Err(err) => return Err(err),
        }
    }

    if errors.is_empty() {
        Ok(output)
    } else {
        Err(ValError::LineErrors(errors))
    }
}

pub trait BuildSet {
    fn build_add(&self, item: Py<PyAny>) -> PyResult<()>;

    fn build_len(&self) -> usize;
}

impl BuildSet for Bound<'_, PySet> {
    fn build_add(&self, item: Py<PyAny>) -> PyResult<()> {
        self.add(item)
    }

    fn build_len(&self) -> usize {
        self.len()
    }
}

impl BuildSet for Bound<'_, PyFrozenSet> {
    fn build_add(&self, item: Py<PyAny>) -> PyResult<()> {
        py_error_on_minusone(self.py(), unsafe {
            // Safety: self.as_ptr() the _only_ pointer to the `frozenset`, and it's allowed
            // to mutate this via the C API when nothing else can refer to it.
            ffi::PySet_Add(self.as_ptr(), item.as_ptr())
        })
    }

    fn build_len(&self) -> usize {
        self.len()
    }
}

fn validate_add<'py>(
    py: Python<'py>,
    set: &impl BuildSet,
    item: impl BorrowInput<'py>,
    state: &mut ValidationState<'_, 'py>,
    validator: &CombinedValidator,
) -> ValResult<()> {
    let validated_item = validator.validate(py, item.borrow_input(), state)?;
    match set.build_add(validated_item) {
        Ok(()) => Ok(()),
        Err(err) => {
            if err.matches(py, py.get_type::<PyTypeError>())? {
                return Err(ValError::new(ErrorTypeDefaults::SetItemNotHashable, item));
            }
            Err(err)?
        }
    }
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn validate_iter_to_set<'py>(
    py: Python<'py>,
    set: &impl BuildSet,
    iter: impl Iterator<Item = PyResult<impl BorrowInput<'py>>>,
    input: &(impl Input<'py> + ?Sized),
    field_type: &'static str,
    max_length: Option<usize>,
    validator: &CombinedValidator,
    state: &mut ValidationState<'_, 'py>,
    fail_fast: bool,
) -> ValResult<()> {
    let mut errors: Vec<ValLineError> = Vec::new();

    let allow_partial = state.allow_partial;

    for (index, is_last_partial, item_result) in state.enumerate_last_partial(iter) {
        state.allow_partial = match is_last_partial {
            true => allow_partial,
            false => PartialMode::Off,
        };
        let item = item_result.map_err(|e| any_next_error!(py, e, input, index))?;
        match validate_add(py, set, item, state, validator) {
            Ok(()) => {
                if let Some(max_length) = max_length
                    && set.build_len() > max_length
                {
                    return Err(ValError::new(
                        ErrorType::TooLong {
                            field_type: field_type.to_string(),
                            max_length,
                            // The logic here is that it doesn't matter how many elements the
                            // input actually had; all we know is it had more than the allowed
                            // number of deduplicated elements.
                            actual_length: None,
                            context: None,
                        },
                        input,
                    ));
                }
            }
            Err(ValError::LineErrors(line_errors)) => {
                if !is_last_partial {
                    errors.extend(line_errors.into_iter().map(|err| err.with_outer_location(index)));
                }
            }
            Err(ValError::Omit) => (),
            Err(err) => return Err(err),
        }
        if fail_fast && !errors.is_empty() {
            return Err(ValError::LineErrors(errors));
        }
    }

    if errors.is_empty() {
        Ok(())
    } else {
        Err(ValError::LineErrors(errors))
    }
}

pub(crate) fn no_validator_iter_to_vec<'py>(
    py: Python<'py>,
    input: &(impl Input<'py> + ?Sized),
    iter: impl Iterator<Item = PyResult<impl BorrowInput<'py>>>,
    mut max_length_check: MaxLengthCheck<'_, impl Input<'py> + ?Sized>,
) -> ValResult<Vec<Py<PyAny>>> {
    iter.enumerate()
        .map(|(index, result)| {
            let v = result.map_err(|e| any_next_error!(py, e, input, index))?;
            max_length_check.incr()?;
            Ok(v.borrow_input().to_object(py)?.unbind())
        })
        .collect()
}

pub(crate) fn iterate_mapping_items<'a, 'py>(
    mapping: &'a Bound<'py, PyMapping>,
) -> ValResult<impl Iterator<Item = ValResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>> + 'a> {
    let py = mapping.py();
    let input = mapping.as_any();
    let iterator = mapping
        .items()
        .map_err(|e| mapping_err(e, py, input))?
        .iter()
        .map(move |item| {
            item.extract().map_err(|_| {
                ValError::new(
                    ErrorType::MappingType {
                        error: MAPPING_TUPLE_ERROR.into(),
                        context: None,
                    },
                    input,
                )
            })
        });
    Ok(iterator)
}

fn mapping_err<'py>(err: PyErr, py: Python<'py>, input: &'py (impl Input<'py> + ?Sized)) -> ValError {
    ValError::new(
        ErrorType::MappingType {
            error: py_err_string(py, err).into(),
            context: None,
        },
        input,
    )
}

const MAPPING_TUPLE_ERROR: &str = "Mapping items must be tuples of (key, value) pairs";

/// Iterate over attributes of an object
pub(crate) fn iterate_attributes<'a, 'py>(
    object: &'a Bound<'py, PyAny>,
) -> PyResult<impl Iterator<Item = ValResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>> + 'a> {
    Ok(object.dir()?.into_iter().filter_map(|attr| {
        let name = match attr.cast_into::<PyString>() {
            Ok(name) => name,
            Err(e) => return Some(Err(e.into())),
        };

        // skip private attributes
        // from benchmarks this is 14x faster than using the python `startswith` method
        if !name.to_string_lossy().starts_with('_')
                // getattr is most likely to fail due to an exception in a @property, skip
                && let Ok(attr) = object.getattr(&name)
                // we don't want bound methods to be included, is there a better way to check?
                // ref https://stackoverflow.com/a/18955425/949890
                && matches!(attr.hasattr(intern!(attr.py(), "__self__")), Ok(true))
                // the PyFunction::is_type_of(attr) catches `staticmethod`, but also any other function,
                // I think that's better than including static methods in the yielded attributes,
                // if someone really wants fields, they can use an explicit field, or a function to modify input
                && !attr.is_instance_of::<PyFunction>()
        {
            return Some(Ok((name.into_any(), attr)));
        }

        None
    }))
}

#[derive(Debug)]
pub enum GenericIterator<'data> {
    PyIterator(GenericPyIterator),
    JsonArray(GenericJsonIterator<'data>),
}

impl PyGcTraverse for GenericIterator<'_> {
    fn py_gc_traverse(&self, visit: &pyo3::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> {
        if let Self::PyIterator(iter) = self {
            iter.py_gc_traverse(visit)?;
        }
        Ok(())
    }
}

impl GenericIterator<'_> {
    pub(crate) fn into_static(self) -> GenericIterator<'static> {
        match self {
            GenericIterator::PyIterator(iter) => GenericIterator::PyIterator(iter),
            GenericIterator::JsonArray(iter) => GenericIterator::JsonArray(iter.into_static()),
        }
    }
}

impl<'data> From<JsonArray<'data>> for GenericIterator<'data> {
    fn from(array: JsonArray<'data>) -> Self {
        let json_iter = GenericJsonIterator { array, index: 0 };
        Self::JsonArray(json_iter)
    }
}

impl From<&Bound<'_, PyAny>> for GenericIterator<'_> {
    fn from(obj: &Bound<'_, PyAny>) -> Self {
        let py_iter = GenericPyIterator {
            obj: obj.clone().into(),
            iter: obj.try_iter().unwrap().into(),
            index: 0,
        };
        Self::PyIterator(py_iter)
    }
}

#[derive(Debug, Clone)]
pub struct GenericPyIterator {
    obj: Py<PyAny>,
    iter: Py<PyIterator>,
    index: usize,
}

impl GenericPyIterator {
    pub fn next<'py>(&mut self, py: Python<'py>) -> PyResult<Option<(Bound<'py, PyAny>, usize)>> {
        // TODO: .to_owned() is a workaround for PyO3 not allowing `.next()` to be called on
        // the reference
        match self.iter.bind(py).to_owned().next() {
            Some(Ok(next)) => {
                let a = (next, self.index);
                self.index += 1;
                Ok(Some(a))
            }
            Some(Err(err)) => Err(err),
            None => Ok(None),
        }
    }

    pub fn input_as_error_value(&self, py: Python<'_>) -> InputValue {
        InputValue::Python(self.obj.clone_ref(py))
    }

    pub fn index(&self) -> usize {
        self.index
    }
}

impl_py_gc_traverse!(GenericPyIterator { obj, iter });

#[derive(Debug, Clone)]
pub struct GenericJsonIterator<'data> {
    array: JsonArray<'data>,
    index: usize,
}

impl<'data> GenericJsonIterator<'data> {
    pub fn next(&mut self, _py: Python) -> PyResult<Option<(&JsonValue<'data>, usize)>> {
        if self.index < self.array.len() {
            // panic here is impossible due to bounds check above; compiler should be
            // able to optimize it away even
            let next = &self.array[self.index];
            let a = (next, self.index);
            self.index += 1;
            Ok(Some(a))
        } else {
            Ok(None)
        }
    }

    pub fn input_as_error_value(&self, _py: Python<'_>) -> InputValue {
        InputValue::Json(JsonValue::Array(self.array.clone()).into_static())
    }

    pub fn index(&self) -> usize {
        self.index
    }

    pub fn into_static(self) -> GenericJsonIterator<'static> {
        GenericJsonIterator {
            array: JsonArray::new(self.array.iter().map(JsonValue::to_static).collect()),
            index: self.index,
        }
    }
}

#[cfg_attr(debug_assertions, derive(Debug))]
pub enum EitherString<'a, 'py> {
    Cow(Cow<'a, str>),
    Py(Bound<'py, PyString>),
}

impl<'py> EitherString<'_, 'py> {
    pub fn as_cow(&self) -> ValResult<Cow<'_, str>> {
        match self {
            Self::Cow(data) => Ok(Cow::Borrowed(data)),
            Self::Py(py_str) => Ok(Cow::Borrowed(py_string_str(py_str)?)),
        }
    }

    pub fn as_py_string(&self, py: Python<'py>, cache_str: StringCacheMode) -> Bound<'py, PyString> {
        match self {
            Self::Cow(cow) => new_py_string(py, cow.as_ref(), cache_str),
            Self::Py(py_string) => py_string.clone(),
        }
    }
}

impl<'a> From<&'a str> for EitherString<'a, '_> {
    fn from(data: &'a str) -> Self {
        Self::Cow(Cow::Borrowed(data))
    }
}

impl From<String> for EitherString<'_, '_> {
    fn from(data: String) -> Self {
        Self::Cow(Cow::Owned(data))
    }
}

impl<'py> From<Bound<'py, PyString>> for EitherString<'_, 'py> {
    fn from(date: Bound<'py, PyString>) -> Self {
        Self::Py(date)
    }
}

pub fn py_string_str<'a>(py_str: &'a Bound<'_, PyString>) -> ValResult<&'a str> {
    py_str.to_str().map_err(|_| {
        ValError::new_custom_input(
            ErrorTypeDefaults::StringUnicode,
            InputValue::Python(py_str.clone().into()),
        )
    })
}

#[cfg_attr(debug_assertions, derive(Debug))]
#[derive(IntoPyObject)]
pub enum EitherBytes<'a, 'py> {
    Cow(Cow<'a, [u8]>),
    Py(Bound<'py, PyBytes>),
}

impl From<Vec<u8>> for EitherBytes<'_, '_> {
    fn from(bytes: Vec<u8>) -> Self {
        Self::Cow(Cow::Owned(bytes))
    }
}

impl<'a> From<&'a [u8]> for EitherBytes<'a, '_> {
    fn from(bytes: &'a [u8]) -> Self {
        Self::Cow(Cow::Borrowed(bytes))
    }
}

impl<'a, 'py> From<&'a Bound<'py, PyBytes>> for EitherBytes<'a, 'py> {
    fn from(bytes: &'a Bound<'py, PyBytes>) -> Self {
        Self::Py(bytes.clone())
    }
}

impl EitherBytes<'_, '_> {
    pub fn as_slice(&self) -> &[u8] {
        match self {
            EitherBytes::Cow(bytes) => bytes,
            EitherBytes::Py(py_bytes) => py_bytes.as_bytes(),
        }
    }

    pub fn len(&self) -> PyResult<usize> {
        match self {
            EitherBytes::Cow(bytes) => Ok(bytes.len()),
            EitherBytes::Py(py_bytes) => py_bytes.len(),
        }
    }
}

#[cfg_attr(debug_assertions, derive(Debug))]
#[derive(IntoPyObject)]
pub enum EitherInt<'py> {
    I64(i64),
    U64(u64),
    BigInt(BigInt),
    Py(Bound<'py, PyAny>),
}

impl<'py> EitherInt<'py> {
    pub fn upcast(py_any: &Bound<'py, PyAny>) -> ValResult<Self> {
        if let Ok(int_64) = py_any.extract() {
            Ok(Self::I64(int_64))
        } else {
            let big_int: BigInt = py_any.extract()?;
            Ok(Self::BigInt(big_int))
        }
    }

    pub fn into_i64(self, py: Python<'py>) -> ValResult<i64> {
        match self {
            EitherInt::I64(i) => Ok(i),
            EitherInt::U64(u) => match i64::try_from(u) {
                Ok(u) => Ok(u),
                Err(_) => Err(ValError::new(
                    ErrorTypeDefaults::IntParsingSize,
                    u.into_bound_py_any(py)?,
                )),
            },
            EitherInt::BigInt(u) => match i64::try_from(u) {
                Ok(u) => Ok(u),
                Err(e) => Err(ValError::new(
                    ErrorTypeDefaults::IntParsingSize,
                    e.into_original().into_bound_py_any(py)?,
                )),
            },
            EitherInt::Py(i) => i
                .extract()
                .map_err(|_| ValError::new(ErrorTypeDefaults::IntParsingSize, &i)),
        }
    }

    pub fn as_int(&self) -> ValResult<Int> {
        match self {
            EitherInt::I64(i) => Ok(Int::I64(*i)),
            EitherInt::U64(u) => match i64::try_from(*u) {
                Ok(i) => Ok(Int::I64(i)),
                Err(_) => Ok(Int::Big(BigInt::from(*u))),
            },
            EitherInt::BigInt(b) => Ok(Int::Big(b.clone())),
            EitherInt::Py(i) => i
                .extract()
                .map_err(|_| ValError::new(ErrorTypeDefaults::IntParsingSize, i)),
        }
    }

    pub fn as_bool(&self) -> Option<bool> {
        match self {
            EitherInt::I64(i) => match i {
                0 => Some(false),
                1 => Some(true),
                _ => None,
            },
            EitherInt::U64(u) => match u {
                0 => Some(false),
                1 => Some(true),
                _ => None,
            },
            EitherInt::BigInt(i) => match u8::try_from(i) {
                Ok(0) => Some(false),
                Ok(1) => Some(true),
                _ => None,
            },
            EitherInt::Py(i) => match i.extract::<u8>() {
                Ok(0) => Some(false),
                Ok(1) => Some(true),
                _ => None,
            },
        }
    }
}

#[cfg_attr(debug_assertions, derive(Debug))]
#[derive(Clone, IntoPyObject)]
pub enum EitherFloat<'py> {
    F64(f64),
    Py(Bound<'py, PyFloat>),
}

impl EitherFloat<'_> {
    pub fn as_f64(&self) -> f64 {
        match self {
            EitherFloat::F64(f) => *f,
            EitherFloat::Py(f) => f.value(),
        }
    }
}

#[derive(Debug, Clone, Serialize, IntoPyObject)]
#[serde(untagged)]
pub enum Int {
    I64(i64),
    #[serde(serialize_with = "serialize_bigint_as_number")]
    Big(BigInt),
}

// The default serialization for BigInt is some internal representation which roundtrips efficiently
// but is not the JSON value which users would expect to see.
fn serialize_bigint_as_number<S>(big_int: &BigInt, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serde_json::Number::from_str(&big_int.to_string())
        .map_err(S::Error::custom)
        .expect("a valid number")
        .serialize(serializer)
}

impl PartialOrd for Int {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match (self, other) {
            (Int::I64(i1), Int::I64(i2)) => Some(i1.cmp(i2)),
            (Int::Big(b1), Int::Big(b2)) => Some(b1.cmp(b2)),
            (Int::I64(i), Int::Big(b)) => Some(BigInt::from(*i).cmp(b)),
            (Int::Big(b), Int::I64(i)) => Some(b.cmp(&BigInt::from(*i))),
        }
    }
}

impl PartialEq for Int {
    fn eq(&self, other: &Self) -> bool {
        self.partial_cmp(other) == Some(Ordering::Equal)
    }
}

impl Rem for &Int {
    type Output = Int;

    fn rem(self, rhs: Self) -> Self::Output {
        match (self, rhs) {
            (Int::I64(i1), Int::I64(i2)) => Int::I64(i1 % i2),
            (Int::Big(b1), Int::Big(b2)) => Int::Big(b1 % b2),
            (Int::I64(i), Int::Big(b)) => Int::Big(BigInt::from(*i) % b),
            (Int::Big(b), Int::I64(i)) => Int::Big(b % BigInt::from(*i)),
        }
    }
}

impl FromPyObject<'_, '_> for Int {
    type Error = PyErr;

    #[allow(clippy::same_functions_in_if_condition)] // https://github.com/rust-lang/rust-clippy/issues/10928
    fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
        if let Ok(i) = obj.extract() {
            Ok(Int::I64(i))
        } else if let Ok(b) = obj.extract() {
            Ok(Int::Big(b))
        } else {
            Err(PyTypeError::new_err(format!("Expected int, got {}", obj.get_type())))
        }
    }
}

#[derive(Clone)]
pub enum EitherComplex<'py> {
    Complex([f64; 2]),
    Py(Bound<'py, PyComplex>),
}

impl<'py> IntoPyObject<'py> for EitherComplex<'py> {
    type Target = PyComplex;
    type Output = Bound<'py, PyComplex>;
    type Error = Infallible;

    fn into_pyobject(self, py: Python<'py>) -> Result<Bound<'py, PyComplex>, Infallible> {
        match self {
            EitherComplex::Complex(c) => Ok(PyComplex::from_doubles(py, c[0], c[1])),
            EitherComplex::Py(c) => Ok(c),
        }
    }
}
