use polars_error::{PolarsResult, polars_bail};
use polars_utils::pl_str::PlSmallStr;

use crate::config;

#[derive(Clone, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
pub struct TimeZone {
    /// Private inner to ensure canonical / parsed time zone repr at construction.
    inner: PlSmallStr,
}

impl TimeZone {
    pub const UTC: TimeZone = unsafe { TimeZone::from_static("UTC") };

    /// Construct from a static string.
    ///
    /// # Safety
    /// This does not perform any validation, the caller is responsible for
    /// ensuring they pass a valid timezone.
    #[inline(always)]
    pub const unsafe fn from_static(tz: &'static str) -> Self {
        Self {
            inner: PlSmallStr::from_static(tz),
        }
    }

    /// # Safety
    /// This does not perform any validation, the caller is responsible for
    /// ensuring they pass a valid timezone.
    pub unsafe fn new_unchecked(zone_str: impl Into<PlSmallStr>) -> Self {
        Self {
            inner: zone_str.into(),
        }
    }

    /// Converts timezones to canonical form.
    ///
    /// If the "timezones" feature is enabled, additionally performs validation and converts to
    /// Etc/GMT form where applicable.
    #[inline]
    pub fn opt_try_new(zone_str: Option<impl Into<PlSmallStr>>) -> PolarsResult<Option<Self>> {
        Self::new_impl(zone_str.map(|x| x.into()))
    }

    fn new_impl(zone_str: Option<PlSmallStr>) -> PolarsResult<Option<Self>> {
        // Needed for selectors https://github.com/pola-rs/polars/pull/9641
        if zone_str.as_deref() == Some("*") {
            return Ok(Some(Self {
                inner: PlSmallStr::from_static("*"),
            }));
        }

        let mut canonical_tz = Self::_canonical_timezone_impl(zone_str);

        #[cfg(feature = "timezones")]
        if let Some(tz) = canonical_tz.as_mut() {
            if let Err(err) = Self::validate_time_zone(tz) {
                match parse_fixed_offset(tz) {
                    Ok(v) => *tz = v,
                    Err(_) => {
                        // This can be used if there are externally created arrow buffers / dtypes
                        // with unknown timezones.
                        if std::env::var("POLARS_IGNORE_TIMEZONE_PARSE_ERROR").as_deref() == Ok("1")
                        {
                            if config::verbose() {
                                eprintln!("WARN: {err}")
                            }
                        } else {
                            return Err(err.wrap_msg(|s| {
                                format!(
                                    "{s}\n\nIf you would like to forcibly disable \
                                    timezone validation, set \
                                    POLARS_IGNORE_TIMEZONE_PARSE_ERROR=1.",
                                )
                            }));
                        }
                    },
                }
            }
        }

        Ok(canonical_tz.map(|inner| Self { inner }))
    }

    /// Equality where `None` is treated as UTC.
    pub fn eq_none_as_utc(this: Option<&TimeZone>, other: Option<&TimeZone>) -> bool {
        this.unwrap_or(&Self::UTC) == other.unwrap_or(&Self::UTC)
    }

    pub fn _canonical_timezone_impl(tz: Option<PlSmallStr>) -> Option<PlSmallStr> {
        match tz.as_deref() {
            Some("") | None => None,
            #[cfg(feature = "timezones")]
            Some("+00:00") | Some("00:00") | Some("utc") => Some(PlSmallStr::from_static("UTC")),
            Some(_) => tz,
        }
    }

    #[cfg(feature = "timezones")]
    pub fn from_chrono(tz: &chrono_tz::Tz) -> Self {
        use polars_utils::format_pl_smallstr;

        Self {
            inner: format_pl_smallstr!("{}", tz),
        }
    }

    #[cfg(feature = "timezones")]
    pub fn to_chrono(&self) -> PolarsResult<chrono_tz::Tz> {
        parse_time_zone(self)
    }

    #[cfg(feature = "timezones")]
    pub fn validate_time_zone(tz: &str) -> PolarsResult<()> {
        parse_time_zone(tz).map(|_| ())
    }
}

impl std::ops::Deref for TimeZone {
    type Target = PlSmallStr;

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

impl std::fmt::Debug for TimeZone {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        std::fmt::Debug::fmt(&self.inner, f)
    }
}

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

#[cfg(feature = "timezones")]
static FIXED_OFFSET_PATTERN: &str = r#"(?x)
    ^
    (?P<sign>[-+])?            # optional sign
    (?P<hour>0[0-9]|1[0-4])    # hour (between 0 and 14)
    :?                         # optional separator
    00                         # minute
    $
    "#;

#[cfg(feature = "timezones")]
polars_utils::regex_cache::cached_regex! {
    static FIXED_OFFSET_RE = FIXED_OFFSET_PATTERN;
}

// Implementation from: https://github.com/wooorm/levenshtein-rs/blob/9c4730b1973d4e61e187f8fe6d5f299ad5c991fc/src/lib.rs
// (MIT licensed. Copyright (c) 2016 Titus Wormer) <tituswormer@gmail.com>
fn _levenshtein(a: &str, b: &str) -> usize {
    let mut result = 0;

    /* Shortcut optimizations / degenerate cases. */
    if a == b {
        return result;
    }

    let length_a = a.chars().count();
    let length_b = b.chars().count();

    if length_a == 0 {
        return length_b;
    }

    if length_b == 0 {
        return length_a;
    }

    /* Initialize the vector.
     *
     * This is why it’s fast, normally a matrix is used,
     * here we use a single vector. */
    let mut cache: Vec<usize> = (1..).take(length_a).collect();

    /* Loop. */
    for (index_b, code_b) in b.chars().enumerate() {
        result = index_b;
        let mut distance_a = index_b;

        for (index_a, code_a) in a.chars().enumerate() {
            let distance_b = if code_a == code_b {
                distance_a
            } else {
                distance_a + 1
            };

            distance_a = cache[index_a];

            result = if distance_a > result {
                if distance_b > result {
                    result + 1
                } else {
                    distance_b
                }
            } else if distance_b > distance_a {
                distance_a + 1
            } else {
                distance_b
            };

            cache[index_a] = result;
        }
    }

    result
}

/// Parse a time zone string to [`chrono_tz::Tz`]
#[cfg(feature = "timezones")]
pub fn parse_time_zone(tz: &str) -> PolarsResult<chrono_tz::Tz> {
    if let Ok(tz_parsed) = tz.parse::<chrono_tz::Tz>() {
        return Ok(tz_parsed);
    }

    let mut best: Option<(&str, usize)> = None;
    for &candidate in chrono_tz::TZ_VARIANTS.iter() {
        let score = _levenshtein(tz, candidate.name());
        if best.is_none() || score < best.unwrap().1 {
            best = Some((candidate.name(), score));
        }
    }

    polars_bail!(
        ComputeError:
        "unable to parse time zone: '{tz}'. Please check the \
        Time Zone Database for a list of available time zones.\n\nHint: did you mean '{}' instead?",
        best.unwrap().0,
    )
}

/// Convert fixed offset to Etc/GMT one from time zone database
///
/// E.g. +01:00 -> Etc/GMT-1
///
/// Note: the sign appears reversed, but is correct, see <https://en.wikipedia.org/wiki/Tz_database#Area>:
/// > In order to conform with the POSIX style, those zone names beginning with
/// > "Etc/GMT" have their sign reversed from the standard ISO 8601 convention.
/// > In the "Etc" area, zones west of GMT have a positive sign and those east
/// > have a negative sign in their name (e.g "Etc/GMT-14" is 14 hours ahead of GMT).
#[cfg(feature = "timezones")]
pub fn parse_fixed_offset(tz: &str) -> PolarsResult<PlSmallStr> {
    use polars_utils::format_pl_smallstr;

    if let Some(caps) = FIXED_OFFSET_RE.captures(tz) {
        let sign = match caps.name("sign").map(|s| s.as_str()) {
            Some("-") => "+",
            _ => "-",
        };
        let hour = caps.name("hour").unwrap().as_str().parse::<i32>().unwrap();
        let etc_tz = format_pl_smallstr!("Etc/GMT{}{}", sign, hour);
        if etc_tz.parse::<chrono_tz::Tz>().is_ok() {
            return Ok(etc_tz);
        }
    }

    unable_to_parse_err(tz)
}

#[cfg(feature = "timezones")]
fn unable_to_parse_err<T>(tz: &str) -> PolarsResult<T> {
    polars_bail!(
        ComputeError:
        "unable to parse time zone: '{}'. Please check the \
        Time Zone Database for a list of available time zones.",
        tz
    )
}
