use std::borrow::Borrow;
use std::error::Error;
use std::fmt;
use std::str::FromStr;

use deserr::Deserr;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::error::{Code, ErrorCode};

#[derive(
    Debug, Clone, Deserialize, PartialEq, Eq, Deserr, PartialOrd, Ord, Serialize, ToSchema,
)]
#[deserr(try_from(String) = IndexUid::try_from -> IndexUidFormatError)]
#[serde(try_from = "String")]
#[schema(value_type = String, example = "movies")]
pub struct IndexUid(String);

impl IndexUid {
    pub fn new_unchecked(s: impl AsRef<str>) -> Self {
        Self(s.as_ref().to_string())
    }

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

    /// Return a reference over the inner str.
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Display for IndexUid {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.0, f)
    }
}

impl std::ops::Deref for IndexUid {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl TryFrom<String> for IndexUid {
    type Error = IndexUidFormatError;

    fn try_from(uid: String) -> Result<Self, Self::Error> {
        if !uid.chars().all(|x| x.is_ascii_alphanumeric() || x == '-' || x == '_')
            || uid.is_empty()
            || uid.len() > 400
        {
            Err(IndexUidFormatError { invalid_uid: uid })
        } else {
            Ok(IndexUid(uid))
        }
    }
}

impl FromStr for IndexUid {
    type Err = IndexUidFormatError;

    fn from_str(uid: &str) -> Result<IndexUid, IndexUidFormatError> {
        uid.to_string().try_into()
    }
}

impl From<IndexUid> for String {
    fn from(uid: IndexUid) -> Self {
        uid.into_inner()
    }
}

impl Borrow<String> for IndexUid {
    fn borrow(&self) -> &String {
        &self.0
    }
}

#[derive(Debug)]
pub struct IndexUidFormatError {
    pub invalid_uid: String,
}

impl fmt::Display for IndexUidFormatError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "`{}` is not a valid index uid. Index uid can be an \
            integer or a string containing only alphanumeric \
            characters, hyphens (-) and underscores (_), \
            and can not be more than 512 bytes.",
            self.invalid_uid,
        )
    }
}

impl Error for IndexUidFormatError {}

impl ErrorCode for IndexUidFormatError {
    fn error_code(&self) -> Code {
        Code::InvalidIndexUid
    }
}
