use magnetar_core::web_model::acct::Acct;
use magnetar_sdk::mmm;
use magnetar_sdk::mmm::Token;
use percent_encoding::percent_decode_str;
use std::fmt::Display;
use std::str::FromStr;
use thiserror::Error;
use url::Host;

#[derive(Clone, Debug)]
pub struct ValidName(String);

impl FromStr for ValidName {
    type Err = FediverseTagParseError;

    fn from_str(name: &str) -> Result<Self, Self::Err> {
        if name
            .chars()
            .any(|c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.')
        {
            return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
        }

        Ok(Self(name.to_owned()))
    }
}

impl AsRef<str> for ValidName {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

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

#[derive(Clone, Debug)]
pub struct FediverseTag {
    pub name: ValidName,
    pub host: Option<Host>,
}

impl FediverseTag {
    pub fn from_parts(parts: (&str, Option<&str>)) -> Result<Self, FediverseTagParseError> {
        Self::try_from(FediverseTagDisplay::from_parts(parts)?)
    }
}

impl TryFrom<FediverseTagDisplay> for FediverseTag {
    type Error = FediverseTagParseError;

    fn try_from(
        FediverseTagDisplay { name, host }: FediverseTagDisplay,
    ) -> Result<Self, FediverseTagParseError> {
        Ok(FediverseTag {
            name,
            host: host.as_deref().map(Host::parse).transpose()?,
        })
    }
}

impl TryFrom<&FediverseTagDisplay> for FediverseTag {
    type Error = FediverseTagParseError;

    fn try_from(
        FediverseTagDisplay { name, host }: &FediverseTagDisplay,
    ) -> Result<Self, FediverseTagParseError> {
        Ok(FediverseTag {
            name: name.clone(),
            host: host.as_deref().map(Host::parse).transpose()?,
        })
    }
}

impl From<&FediverseTag> for Acct {
    fn from(tag: &FediverseTag) -> Self {
        Acct::new(tag.to_string().into())
    }
}

impl FromStr for FediverseTag {
    type Err = FediverseTagParseError;

    fn from_str(tag: &str) -> Result<Self, Self::Err> {
        Self::try_from(FediverseTagDisplay::from_str(tag)?)
    }
}

impl Display for FediverseTag {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(ref host) = self.host {
            write!(f, "{}@{}", self.name, host)
        } else {
            write!(f, "{}", self.name)
        }
    }
}

#[derive(Clone, Debug)]
pub struct FediverseTagDisplay {
    pub name: ValidName,
    pub host: Option<String>,
}

impl FediverseTagDisplay {
    pub fn from_parts((name, host): (&str, Option<&str>)) -> Result<Self, FediverseTagParseError> {
        Ok(FediverseTagDisplay {
            name: name.parse()?,
            host: host.map(str::to_owned),
        })
    }
}

impl From<&FediverseTag> for FediverseTagDisplay {
    fn from(FediverseTag { name, host }: &FediverseTag) -> Self {
        FediverseTagDisplay {
            name: name.clone(),
            host: host
                .as_ref()
                .map(Host::to_string)
                .as_deref()
                .map(idna::domain_to_unicode)
                .map(|v| v.0),
        }
    }
}

impl TryFrom<&Acct> for FediverseTagDisplay {
    type Error = FediverseTagParseError;

    fn try_from(acct: &Acct) -> Result<Self, Self::Error> {
        acct.as_ref().parse()
    }
}

impl FromStr for FediverseTagDisplay {
    type Err = FediverseTagParseError;

    fn from_str(tag: &str) -> Result<Self, Self::Err> {
        Self::from_parts(split_tag_inner(tag))
    }
}

impl Display for FediverseTagDisplay {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(ref host) = self.host {
            write!(f, "{}@{}", self.name, host)
        } else {
            write!(f, "{}", self.name)
        }
    }
}

#[derive(Debug, Error)]
pub enum FediverseTagParseError {
    #[error("Invalid char in tag: {0}")]
    InvalidChar(String),
    #[error("Invalid UTF-8 in tag: {0}")]
    InvalidUtf8(#[from] std::str::Utf8Error),
    #[error("Invalid host in tag: {0}")]
    InvalidHost(#[from] url::ParseError),
}

impl From<&FediverseTagParseError> for &str {
    fn from(_: &FediverseTagParseError) -> Self {
        "FediverseTagParseError"
    }
}

impl From<FediverseTag> for Acct {
    fn from(value: FediverseTag) -> Self {
        value.to_string().into()
    }
}

fn split_tag_inner(tag: &str) -> (&str, Option<&str>) {
    let tag = tag.strip_prefix('@').unwrap_or(tag.as_ref());

    match tag.split_once('@') {
        Some((name, "")) => (name, None),
        Some((name, host)) => (name, Some(host)),
        None => (tag, None),
    }
}

pub fn lenient_parse_tag_decode(
    tag: impl AsRef<str>,
) -> Result<FediverseTag, FediverseTagParseError> {
    percent_decode_str(tag.as_ref()).decode_utf8()?.parse()
}

pub fn lenient_parse_acct_decode(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
    lenient_parse_tag_decode(acct.as_ref())
}

#[derive(Debug)]
pub enum RawReaction {
    Unicode(String),
    Shortcode {
        shortcode: String,
        host: Option<String>,
    },
}

pub fn parse_reaction(tag: impl AsRef<str>) -> Option<RawReaction> {
    let tok = mmm::Context::default().parse_ui(tag.as_ref());

    match tok {
        Token::UnicodeEmoji(text) => Some(RawReaction::Unicode(text)),
        Token::ShortcodeEmoji { shortcode, host } => {
            Some(RawReaction::Shortcode { shortcode, host })
        }
        _ => None,
    }
}
