use fancy_regex::{Regex, RegexBuilder};
use serde::{Deserialize, Deserializer};

use crate::config::inclusivities::{
    Inclusivities, is_source_included, is_target_ref_included,
};
use crate::message::Source;
use crate::serde::deserialize_u64_positive_integer_limit;
use crate::target::{self, TargetRef};
use crate::{Server, isupport};

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Preview {
    pub enabled: Enabled,
    pub exclude: Exclude,
    pub max_per_message: usize,
    pub request: Request,
    pub card: Card,
    pub image: Image,
}

impl Default for Preview {
    fn default() -> Self {
        Self {
            enabled: Enabled::default(),
            exclude: Exclude::default(),
            max_per_message: 1,
            request: Request::default(),
            card: Card::default(),
            image: Image::default(),
        }
    }
}

impl Preview {
    pub fn is_enabled(&self, url: &str) -> bool {
        // Check if URL is excluded first
        if self.exclude.is_excluded(url) {
            return false;
        }

        match &self.enabled {
            Enabled::Boolean(b) => *b,
            Enabled::Regex(regexes) => regexes
                .iter()
                .any(|regex| regex.is_match(url).unwrap_or(false)),
        }
    }

    pub fn hide_url_when(&self, preview: &crate::Preview) -> HideUrlCondition {
        match preview {
            crate::Preview::Card(_) => self.card.hide_url,
            crate::Preview::Image(_) => self.image.hide_url,
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct Exclude(Vec<Regex>);

impl Exclude {
    pub fn is_excluded(&self, url: &str) -> bool {
        self.0
            .iter()
            .any(|regex| regex.is_match(url).unwrap_or(false))
    }
}

impl<'de> Deserialize<'de> for Exclude {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let patterns = Vec::<String>::deserialize(deserializer)?;
        let regexes = patterns
            .iter()
            .map(|pattern| {
                RegexBuilder::new(pattern).build().map_err(|err| {
                    serde::de::Error::custom(format!(
                        "invalid regex '{pattern}': {err}"
                    ))
                })
            })
            .collect::<Result<Vec<_>, _>>()?;
        Ok(Exclude(regexes))
    }
}

#[derive(Debug, Clone)]
pub enum Enabled {
    Boolean(bool),
    Regex(Vec<Regex>),
}

impl Default for Enabled {
    fn default() -> Self {
        Self::Boolean(true)
    }
}

impl<'de> Deserialize<'de> for Enabled {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Debug, Clone, Deserialize)]
        #[serde(untagged)]
        pub enum Inner {
            Boolean(bool),
            RegexSingle(String),
            RegexMultiple(Vec<String>),
        }

        match Inner::deserialize(deserializer)? {
            Inner::Boolean(enabled) => Ok(Enabled::Boolean(enabled)),
            Inner::RegexSingle(regex_str) => {
                let regex =
                    RegexBuilder::new(&regex_str).build().map_err(|err| {
                        serde::de::Error::custom(format!(
                            "invalid regex '{regex_str}': {err}"
                        ))
                    })?;

                Ok(Enabled::Regex(vec![regex]))
            }
            Inner::RegexMultiple(regex_strs) => {
                let regexes = regex_strs
                    .iter()
                    .map(|regex_str| {
                        RegexBuilder::new(regex_str).build().map_err(|err| {
                            serde::de::Error::custom(format!(
                                "invalid regex '{regex_str}': {err}"
                            ))
                        })
                    })
                    .collect::<Result<Vec<_>, _>>()?;

                Ok(Enabled::Regex(regexes))
            }
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Request {
    /// Request user agent
    ///
    /// Some servers will only send opengraph metadata to
    /// browser-like user agents. We default to `WhatsApp/2`
    /// for wide compatibility
    pub user_agent: String,
    /// Request timeout in millisceonds
    pub timeout_ms: u64,
    /// Max image size in bytes
    ///
    /// This prevents downloading images that are too big
    pub max_image_size: usize,
    /// Max bytes streamed when scraping for opengraph metadata
    /// before cancelling the request
    ///
    /// This prevents downloading responses that are too big
    pub max_scrape_size: usize,
    /// Number of allowed concurrent requests for fetching previews
    ///
    /// Reduce this to prevent rate-limiting
    pub concurrency: usize,
    /// Number of milliseconds to wait before requesting another preview
    /// when number of requested previews > `concurrency`
    pub delay_ms: u64,
    /// Preview image cache controls.
    pub image_cache: ImageCache,
}

impl Default for Request {
    fn default() -> Self {
        Self {
            user_agent: "WhatsApp/2".to_string(),
            timeout_ms: 10 * 1_000,
            max_image_size: 10 * 1024 * 1024,
            max_scrape_size: 500 * 1024,
            concurrency: 4,
            delay_ms: 500,
            image_cache: ImageCache::default(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct ImageCache {
    /// Maximum image preview cache size in megabytes.
    ///
    /// Oldest cached images are evicted when the cache exceeds this size.
    #[serde(deserialize_with = "deserialize_u64_positive_integer_limit")]
    pub max_size: Option<u64>,
    /// Run image cache trimming every N successful image saves.
    ///
    /// Set to 0 to disable periodic trimming.
    #[serde(deserialize_with = "deserialize_trim_interval")]
    pub trim_interval: u64,
}

impl ImageCache {
    pub fn max_size_bytes(&self) -> Option<u64> {
        self.max_size
            .map(|max_size| max_size.saturating_mul(1_000_000))
    }
}

impl Default for ImageCache {
    fn default() -> Self {
        Self {
            max_size: Some(500),
            trim_interval: 32,
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum HideUrlCondition {
    #[default]
    ContainsOnlyUrl,
    Trailing,
    Never,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Card {
    pub exclude: Option<Inclusivities>,
    pub include: Option<Inclusivities>,
    pub hide_url: HideUrlCondition,
    pub show_image: bool,
    pub round_image_corners: bool,
    /// Maximum width of the card in pixels
    pub max_width: f32,
    /// Maximum height of the description in pixels
    pub description_max_height: f32,
    /// Maximum height of the image in pixels
    pub image_max_height: f32,
}

impl Default for Card {
    fn default() -> Self {
        Self {
            exclude: None,
            include: None,
            hide_url: HideUrlCondition::Never,
            show_image: true,
            round_image_corners: true,
            max_width: 400.0,
            description_max_height: 100.0,
            image_max_height: 200.0,
        }
    }
}

impl Card {
    pub fn visible(
        &self,
        target_ref: TargetRef,
        server: &Server,
        casemapping: isupport::CaseMap,
    ) -> Visibility {
        Visibility::for_target_ref(
            self.include.as_ref(),
            self.exclude.as_ref(),
            target_ref,
            server,
            casemapping,
        )
    }

    pub fn visible_for_source(
        &self,
        source: &Source,
        channel: Option<&target::Channel>,
        server: Option<&Server>,
        casemapping: isupport::CaseMap,
    ) -> bool {
        is_source_included(
            self.include.as_ref(),
            self.exclude.as_ref(),
            source,
            channel,
            server,
            casemapping,
        )
    }
}

#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct Image {
    pub action: ImageAction,
    pub exclude: Option<Inclusivities>,
    pub include: Option<Inclusivities>,
    pub hide_url: HideUrlCondition,
    pub round_corners: bool,
    /// Maximum width of the image in pixels
    pub max_width: f32,
    /// Maximum height of the image in pixels
    pub max_height: f32,
}

impl Default for Image {
    fn default() -> Self {
        Self {
            action: ImageAction::default(),
            exclude: None,
            include: None,
            hide_url: HideUrlCondition::default(),
            round_corners: true,
            max_width: 550.0,
            max_height: 350.0,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ImageAction {
    OpenUrl,
    #[default]
    Preview,
}

impl Image {
    pub fn visible(
        &self,
        target_ref: TargetRef,
        server: &Server,
        casemapping: isupport::CaseMap,
    ) -> Visibility {
        Visibility::for_target_ref(
            self.include.as_ref(),
            self.exclude.as_ref(),
            target_ref,
            server,
            casemapping,
        )
    }

    pub fn visible_for_source(
        &self,
        source: &Source,
        channel: Option<&target::Channel>,
        server: Option<&Server>,
        casemapping: isupport::CaseMap,
    ) -> bool {
        is_source_included(
            self.include.as_ref(),
            self.exclude.as_ref(),
            source,
            channel,
            server,
            casemapping,
        )
    }
}

pub fn deserialize_trim_interval<'de, D>(
    deserializer: D,
) -> Result<u64, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum Inner {
        String(String),
        PositiveInteger(u64),
    }

    match Inner::deserialize(deserializer)? {
        Inner::String(string) => {
            if string == "first-save-only" {
                Ok(0)
            } else {
                Err(serde::de::Error::invalid_value(
                    serde::de::Unexpected::Str(&string),
                    &"first-save-only",
                ))
            }
        }
        Inner::PositiveInteger(integer) => {
            if integer == 0 {
                Err(serde::de::Error::invalid_value(
                    serde::de::Unexpected::Unsigned(integer),
                    &"any positive integer",
                ))
            } else {
                Ok(integer)
            }
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Visibility {
    All,
    BySource,
    None,
}

impl Visibility {
    pub fn for_target_ref(
        include: Option<&Inclusivities>,
        exclude: Option<&Inclusivities>,
        target_ref: TargetRef,
        server: &Server,
        casemapping: isupport::CaseMap,
    ) -> Visibility {
        if is_target_ref_included(
            include,
            Some(&Inclusivities::all()),
            None,
            target_ref,
            server,
            casemapping,
        ) {
            Visibility::All
        } else if !is_target_ref_included(
            None,
            exclude,
            None,
            target_ref,
            server,
            casemapping,
        ) {
            if include.is_some_and(|include| {
                include.has_user_or_server_message_conditions(
                    target_ref,
                    server,
                    casemapping,
                )
            }) {
                Visibility::BySource
            } else {
                Visibility::None
            }
        } else if exclude.is_some_and(|exclude| {
            exclude.has_user_or_server_message_conditions(
                target_ref,
                server,
                casemapping,
            )
        }) {
            Visibility::BySource
        } else {
            Visibility::All
        }
    }
}
