// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use std::{cmp::Ordering, str::FromStr};

use jiff::{Timestamp, Zoned};
use llrt_utils::result::ResultExt;
use rquickjs::{
    atom::PredefinedAtom, class::Trace, prelude::Opt, BigInt, Class, Ctx, Exception, JsLifetime,
    Result, Value,
};

use crate::duration::Duration;
use crate::utils::timestamp_round::TimestampRoundOption;
use crate::zoned_date_time::ZonedDateTime;

use super::extract_bigint_or_number;

#[derive(Clone, JsLifetime, Trace)]
#[rquickjs::class]
pub(crate) struct Instant {
    #[qjs(skip_trace)]
    inner: Timestamp,
}

#[rquickjs::methods(rename_all = "camelCase")]
impl Instant {
    #[qjs(constructor)]
    fn new(ctx: Ctx<'_>, nanos: Value<'_>) -> Result<Self> {
        Self::from_epoch_nanoseconds(ctx, nanos)
    }

    #[qjs(static)]
    fn compare(instant1: Self, instant2: Self) -> i8 {
        match instant1.inner.cmp(&instant2.inner) {
            Ordering::Less => -1,
            Ordering::Equal => 0,
            Ordering::Greater => 1,
        }
    }

    #[qjs(static)]
    fn from(ctx: Ctx<'_>, info: Value<'_>) -> Result<Self> {
        if let Some(obj) = info.as_object() {
            if let Some(cls) = Class::<Self>::from_object(obj) {
                return Ok(cls.borrow().clone());
            }
        }

        let str = info
            .as_string()
            .and_then(|s| s.to_string().ok())
            .or_throw_type(&ctx, "Cannot convert value to string")?;

        Self::from_str(&ctx, &str)
    }

    #[qjs(static)]
    fn from_epoch_milliseconds(ctx: Ctx<'_>, ms: f64) -> Result<Self> {
        let ns = (ms * 1_000_000.0) as i128;
        Self::from_nanosecond(&ctx, ns)
    }

    #[qjs(static)]
    fn from_epoch_nanoseconds(ctx: Ctx<'_>, ns: Value<'_>) -> Result<Self> {
        let ns = extract_bigint_or_number(&ctx, &ns)?;
        Self::from_nanosecond(&ctx, ns)
    }

    fn add(&self, ctx: Ctx<'_>, duration: Value<'_>) -> Result<Self> {
        let duration = Duration::from_value(&ctx, &duration)?;
        let span = duration.into_inner();
        let ts = self.inner.checked_add(span).or_throw_range(&ctx, "")?;
        Ok(Self { inner: ts })
    }

    fn equals(&self, other: Self) -> bool {
        self.inner == other.inner
    }

    fn round(&self, ctx: Ctx<'_>, options: Value<'_>) -> Result<Self> {
        let round = TimestampRoundOption::from_value(&ctx, &options)?;
        let round = round.into_inner();
        let ts = self.inner.round(round).or_throw_range(&ctx, "")?;
        Ok(Self { inner: ts })
    }

    fn since(&self, other: Self) -> Duration {
        Duration::new_object(self.inner - other.inner)
    }

    fn subtract(&self, ctx: Ctx<'_>, duration: Value<'_>) -> Result<Self> {
        let duration = Duration::from_value(&ctx, &duration)?;
        let span = duration.into_inner();
        let ts = self.inner.checked_sub(span).or_throw_range(&ctx, "")?;
        Ok(Self { inner: ts })
    }

    #[allow(clippy::inherent_to_string)]
    #[qjs(rename = PredefinedAtom::ToJSON)]
    fn to_json(&self) -> String {
        self.inner.to_string()
    }

    #[allow(clippy::inherent_to_string)]
    #[qjs(rename = PredefinedAtom::ToString)]
    fn to_string(&self) -> String {
        self.inner.to_string()
    }

    #[qjs(rename = "toZonedDateTimeISO")]
    fn to_zoned_dt_iso(&self, ctx: Ctx<'_>, timezone: Opt<String>) -> Result<ZonedDateTime> {
        let tz = timezone.as_deref().unwrap_or("UTC");
        ZonedDateTime::from_ts_tz(&ctx, &self.inner, tz)
    }

    fn until(&self, other: Self) -> Duration {
        Duration::new_object(other.inner - self.inner)
    }

    fn value_of(&self, ctx: Ctx<'_>) -> Result<()> {
        Err(Exception::throw_type(
            &ctx,
            "can't convert Instant to primitive type",
        ))
    }

    #[qjs(get)]
    fn epoch_milliseconds(&self) -> i64 {
        self.inner.as_millisecond()
    }

    #[qjs(get)]
    fn epoch_nanoseconds<'js>(&self, ctx: Ctx<'js>) -> Result<BigInt<'js>> {
        let ns = self.inner.as_nanosecond();
        let ns = ns.try_into().or_throw_range(&ctx, "")?;
        BigInt::from_i64(ctx, ns)
    }

    #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
    fn to_string_tag() -> &'static str {
        "Temporal.Instant"
    }
}

impl Instant {
    fn from_nanosecond(ctx: &Ctx<'_>, ns: i128) -> Result<Self> {
        let ts = Timestamp::from_nanosecond(ns).or_throw_range(ctx, "")?;
        Ok(Self { inner: ts })
    }

    fn from_str(ctx: &Ctx<'_>, str: &str) -> Result<Self> {
        let ts = Timestamp::from_str(str).or_throw_range(ctx, "")?;
        Ok(Self { inner: ts })
    }

    pub(crate) fn from_zoned(zoned: &Zoned) -> Self {
        let ts = zoned.timestamp();
        Self { inner: ts }
    }

    pub(crate) fn now() -> Self {
        let ts = Timestamp::now();
        Self { inner: ts }
    }
}
