// Copyright 2025 The Drasi Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![allow(clippy::unwrap_used)]
use super::ConversionError;

use std::{
    collections::BTreeMap,
    ops::{Index, IndexMut},
};

use crate::evaluation::variable_value::{
    float::Float, integer::Integer, zoned_datetime::ZonedDateTime as VarZonedDateTime,
    VariableValue,
};

use std::sync::Arc;

use chrono::{DateTime, FixedOffset, NaiveDateTime};
use ordered_float::OrderedFloat;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
pub enum ElementValue {
    #[default]
    Null,
    Bool(bool),
    Float(OrderedFloat<f64>),
    Integer(i64),
    String(Arc<str>),
    List(Vec<ElementValue>),
    Object(ElementPropertyMap),
    LocalDateTime(NaiveDateTime),
    // ElementValue stores zoned datetimes as fixed offsets only; optional
    // IANA timezone names from VariableValue are not represented here.
    ZonedDateTime(DateTime<FixedOffset>),
}

impl From<&ElementPropertyMap> for VariableValue {
    fn from(val: &ElementPropertyMap) -> Self {
        let mut map = BTreeMap::new();
        for (key, value) in val.values.iter() {
            map.insert(key.to_string(), value.into());
        }
        VariableValue::Object(map)
    }
}

impl From<&ElementValue> for VariableValue {
    fn from(val: &ElementValue) -> Self {
        match val {
            ElementValue::Null => VariableValue::Null,
            ElementValue::Bool(b) => VariableValue::Bool(*b),
            ElementValue::Float(f) => VariableValue::Float(Float::from(f.0)),
            ElementValue::Integer(i) => VariableValue::Integer(Integer::from(*i)),
            ElementValue::String(s) => VariableValue::String(s.to_string()),
            ElementValue::List(l) => VariableValue::List(l.iter().map(|x| x.into()).collect()),
            ElementValue::Object(o) => o.into(),
            ElementValue::LocalDateTime(dt) => VariableValue::LocalDateTime(*dt),
            ElementValue::ZonedDateTime(dt) => {
                // ElementValue cannot carry IANA timezone names, so we emit None.
                VariableValue::ZonedDateTime(VarZonedDateTime::new(*dt, None))
            }
        }
    }
}

impl TryInto<ElementValue> for &VariableValue {
    type Error = ConversionError;

    fn try_into(self) -> Result<ElementValue, ConversionError> {
        match self {
            VariableValue::Null => Ok(ElementValue::Null),
            VariableValue::Bool(b) => Ok(ElementValue::Bool(*b)),
            VariableValue::Float(f) => Ok(ElementValue::Float(OrderedFloat(
                f.as_f64().unwrap_or_default(),
            ))),
            VariableValue::Integer(i) => Ok(ElementValue::Integer(i.as_i64().unwrap_or_default())),
            VariableValue::String(s) => Ok(ElementValue::String(Arc::from(s.as_str()))),
            VariableValue::List(l) => Ok(ElementValue::List(
                l.iter().map(|x| x.try_into().unwrap_or_default()).collect(),
            )),
            VariableValue::Object(o) => Ok(ElementValue::Object(o.into())),
            VariableValue::LocalDateTime(dt) => Ok(ElementValue::LocalDateTime(*dt)),
            // Preserve the fixed-offset instant; timezone_name metadata is dropped.
            VariableValue::ZonedDateTime(zdt) => Ok(ElementValue::ZonedDateTime(*zdt.datetime())),
            _ => Err(ConversionError {}),
        }
    }
}

impl TryInto<ElementValue> for VariableValue {
    type Error = ConversionError;

    fn try_into(self) -> Result<ElementValue, ConversionError> {
        match self {
            VariableValue::Null => Ok(ElementValue::Null),
            VariableValue::Bool(b) => Ok(ElementValue::Bool(b)),
            VariableValue::Float(f) => Ok(ElementValue::Float(OrderedFloat(
                f.as_f64().unwrap_or_default(),
            ))),
            VariableValue::Integer(i) => Ok(ElementValue::Integer(i.as_i64().unwrap_or_default())),
            VariableValue::String(s) => Ok(ElementValue::String(Arc::from(s.as_str()))),
            VariableValue::List(l) => Ok(ElementValue::List(
                l.iter().map(|x| x.try_into().unwrap_or_default()).collect(),
            )),
            VariableValue::Object(o) => Ok(ElementValue::Object(o.into())),
            VariableValue::LocalDateTime(dt) => Ok(ElementValue::LocalDateTime(dt)),
            // Preserve the fixed-offset instant; timezone_name metadata is dropped.
            VariableValue::ZonedDateTime(zdt) => Ok(ElementValue::ZonedDateTime(*zdt.datetime())),
            _ => Err(ConversionError {}),
        }
    }
}

/// Tag used inside JSON envelopes to preserve `ElementValue` datetime type
/// information across serialization round-trips.
/// This key is intentionally specific to minimize collisions with user data.
/// This envelope is for internal round-trips, not an external API shape.
const DRASI_TYPE_TAG: &str = "__drasi_v1_type__";
const DRASI_ENVELOPE_MARKER_TAG: &str = "__drasi_v1_envelope__";
const DRASI_ENVELOPE_MARKER_VALUE: &str = "drasi.element_value.datetime";

impl From<&ElementValue> for serde_json::Value {
    fn from(val: &ElementValue) -> Self {
        match val {
            ElementValue::Null => serde_json::Value::Null,
            ElementValue::Bool(b) => serde_json::Value::Bool(*b),
            ElementValue::Float(f) => {
                serde_json::Value::Number(serde_json::Number::from_f64(f.into_inner()).unwrap())
            }
            ElementValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
            ElementValue::String(s) => serde_json::Value::String(s.to_string()),
            ElementValue::List(l) => serde_json::Value::Array(l.iter().map(|x| x.into()).collect()),
            ElementValue::Object(o) => serde_json::Value::Object(o.into()),
            ElementValue::LocalDateTime(dt) => {
                serde_json::json!({
                    DRASI_ENVELOPE_MARKER_TAG: DRASI_ENVELOPE_MARKER_VALUE,
                    DRASI_TYPE_TAG: "drasi.LocalDateTime",
                    "value": dt.to_string()
                })
            }
            ElementValue::ZonedDateTime(dt) => {
                serde_json::json!({
                    DRASI_ENVELOPE_MARKER_TAG: DRASI_ENVELOPE_MARKER_VALUE,
                    DRASI_TYPE_TAG: "drasi.ZonedDateTime",
                    "value": dt.to_rfc3339()
                })
            }
        }
    }
}

impl From<&serde_json::Value> for ElementValue {
    fn from(value: &serde_json::Value) -> Self {
        match value {
            serde_json::Value::Null => ElementValue::Null,
            serde_json::Value::Bool(b) => ElementValue::Bool(*b),
            serde_json::Value::Number(n) => {
                if let Some(i) = n.as_i64() {
                    ElementValue::Integer(i)
                } else if let Some(f) = n.as_f64() {
                    ElementValue::Float(OrderedFloat(f))
                } else {
                    ElementValue::Null
                }
            }
            serde_json::Value::String(s) => ElementValue::String(Arc::from(s.as_str())),
            serde_json::Value::Array(a) => ElementValue::List(a.iter().map(|x| x.into()).collect()),
            serde_json::Value::Object(o) => {
                // Check for tagged datetime envelope
                let has_internal_marker = o.get(DRASI_ENVELOPE_MARKER_TAG).and_then(|v| v.as_str())
                    == Some(DRASI_ENVELOPE_MARKER_VALUE);
                if has_internal_marker {
                    if let Some(type_tag) = o.get(DRASI_TYPE_TAG).and_then(|v| v.as_str()) {
                        if let Some(val_str) = o.get("value").and_then(|v| v.as_str()) {
                            match type_tag {
                                "drasi.LocalDateTime" => {
                                    if let Ok(dt) = NaiveDateTime::parse_from_str(
                                        val_str,
                                        "%Y-%m-%d %H:%M:%S%.f",
                                    ) {
                                        return ElementValue::LocalDateTime(dt);
                                    }
                                }
                                "drasi.ZonedDateTime" => {
                                    if let Ok(dt) = DateTime::parse_from_rfc3339(val_str) {
                                        return ElementValue::ZonedDateTime(dt);
                                    }
                                }
                                _ => {}
                            }
                        }
                    }
                }
                ElementValue::Object(o.into())
            }
        }
    }
}

impl TryInto<ElementPropertyMap> for &VariableValue {
    type Error = ConversionError;

    fn try_into(self) -> Result<ElementPropertyMap, ConversionError> {
        match self {
            VariableValue::Object(o) => {
                let mut values = BTreeMap::new();
                for (key, value) in o.iter() {
                    values.insert(Arc::from(key.as_str()), value.try_into()?);
                }
                Ok(ElementPropertyMap { values })
            }
            _ => Err(ConversionError {}),
        }
    }
}

#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct ElementPropertyMap {
    values: BTreeMap<Arc<str>, ElementValue>,
}

impl Default for ElementPropertyMap {
    fn default() -> Self {
        Self::new()
    }
}

impl ElementPropertyMap {
    pub fn new() -> Self {
        ElementPropertyMap {
            values: BTreeMap::new(),
        }
    }

    pub fn get(&self, key: &str) -> Option<&ElementValue> {
        self.values.get(key)
    }

    pub fn insert(&mut self, key: &str, value: ElementValue) {
        self.values.insert(Arc::from(key), value);
    }

    pub fn merge(&mut self, other: &ElementPropertyMap) {
        for (key, value) in other.values.iter() {
            self.values
                .entry(key.clone())
                .or_insert_with(|| value.clone());
        }
    }

    pub fn map_iter<T>(
        &self,
        f: impl Fn(&Arc<str>, &ElementValue) -> T + 'static,
    ) -> impl Iterator<Item = T> + '_ {
        self.values.iter().map(move |(k, v)| f(k, v))
    }
}

impl Index<&str> for ElementPropertyMap {
    type Output = ElementValue;

    fn index(&self, key: &str) -> &Self::Output {
        static NULL: ElementValue = ElementValue::Null;
        match self.values.get(key) {
            Some(value) => value,
            None => &NULL,
        }
    }
}

impl IndexMut<&str> for ElementPropertyMap {
    fn index_mut(&mut self, key: &str) -> &mut Self::Output {
        self.values
            .entry(Arc::from(key))
            .or_insert_with(|| ElementValue::Null)
    }
}

impl From<&BTreeMap<String, VariableValue>> for ElementPropertyMap {
    fn from(map: &BTreeMap<String, VariableValue>) -> Self {
        let mut values = BTreeMap::new();
        for (key, value) in map.iter() {
            values.insert(Arc::from(key.as_str()), value.try_into().unwrap());
        }
        ElementPropertyMap { values }
    }
}

impl From<BTreeMap<String, VariableValue>> for ElementPropertyMap {
    fn from(map: BTreeMap<String, VariableValue>) -> Self {
        let mut values = BTreeMap::new();
        for (key, value) in map {
            values.insert(Arc::from(key.as_str()), value.try_into().unwrap());
        }
        ElementPropertyMap { values }
    }
}

impl From<BTreeMap<String, ElementValue>> for ElementPropertyMap {
    fn from(map: BTreeMap<String, ElementValue>) -> Self {
        let mut values = BTreeMap::new();
        for (key, value) in map {
            values.insert(Arc::from(key.as_str()), value);
        }
        ElementPropertyMap { values }
    }
}

impl From<&ElementPropertyMap> for serde_json::Map<String, serde_json::Value> {
    fn from(val: &ElementPropertyMap) -> Self {
        val.values
            .iter()
            .map(|(k, v)| (k.to_string(), v.into()))
            .collect()
    }
}

impl From<&serde_json::Map<String, serde_json::Value>> for ElementPropertyMap {
    fn from(map: &serde_json::Map<String, serde_json::Value>) -> Self {
        let mut values = BTreeMap::new();
        for (key, value) in map.iter() {
            values.insert(Arc::from(key.as_str()), value.into());
        }
        ElementPropertyMap { values }
    }
}

impl From<serde_json::Value> for ElementPropertyMap {
    fn from(value: serde_json::Value) -> Self {
        match value {
            serde_json::Value::Object(o) => (&o).into(),
            _ => ElementPropertyMap::new(),
        }
    }
}

impl From<&serde_json::Value> for ElementPropertyMap {
    fn from(value: &serde_json::Value) -> Self {
        match value {
            serde_json::Value::Object(o) => o.into(),
            _ => ElementPropertyMap::new(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::evaluation::variable_value::{
        float::Float, integer::Integer, zoned_datetime::ZonedDateTime as VarZonedDateTime,
        VariableValue,
    };
    use chrono::{NaiveDate, TimeZone, Utc};

    fn sample_naive_datetime() -> NaiveDateTime {
        NaiveDate::from_ymd_opt(2024, 6, 15)
            .unwrap()
            .and_hms_opt(10, 30, 45)
            .unwrap()
    }

    fn sample_fixed_datetime() -> DateTime<FixedOffset> {
        let offset = FixedOffset::east_opt(3600).unwrap(); // +01:00
        offset.with_ymd_and_hms(2024, 6, 15, 10, 30, 45).unwrap()
    }

    // ── ElementValue → VariableValue ────────────────────────────────────

    #[test]
    fn local_datetime_to_variable_value() {
        let dt = sample_naive_datetime();
        let ev = ElementValue::LocalDateTime(dt);
        let vv: VariableValue = (&ev).into();
        assert_eq!(vv, VariableValue::LocalDateTime(dt));
    }

    #[test]
    fn zoned_datetime_to_variable_value() {
        let dt = sample_fixed_datetime();
        let ev = ElementValue::ZonedDateTime(dt);
        let vv: VariableValue = (&ev).into();
        let expected = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
        assert_eq!(vv, expected);
    }

    // ── VariableValue → ElementValue (by ref) ──────────────────────────

    #[test]
    fn variable_value_ref_local_datetime_to_element_value() {
        let dt = sample_naive_datetime();
        let vv = VariableValue::LocalDateTime(dt);
        let ev: ElementValue = (&vv).try_into().unwrap();
        assert_eq!(ev, ElementValue::LocalDateTime(dt));
    }

    #[test]
    fn variable_value_ref_zoned_datetime_to_element_value() {
        let dt = sample_fixed_datetime();
        let vv = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
        let ev: ElementValue = (&vv).try_into().unwrap();
        assert_eq!(ev, ElementValue::ZonedDateTime(dt));
    }

    // ── VariableValue → ElementValue (by value) ────────────────────────

    #[test]
    fn variable_value_owned_local_datetime_to_element_value() {
        let dt = sample_naive_datetime();
        let vv = VariableValue::LocalDateTime(dt);
        let ev: ElementValue = vv.try_into().unwrap();
        assert_eq!(ev, ElementValue::LocalDateTime(dt));
    }

    #[test]
    fn variable_value_owned_zoned_datetime_to_element_value() {
        let dt = sample_fixed_datetime();
        let vv = VariableValue::ZonedDateTime(VarZonedDateTime::new(dt, None));
        let ev: ElementValue = vv.try_into().unwrap();
        assert_eq!(ev, ElementValue::ZonedDateTime(dt));
    }

    // ── ElementValue → serde_json::Value ───────────────────────────────

    #[test]
    fn local_datetime_to_json() {
        let dt = sample_naive_datetime();
        let ev = ElementValue::LocalDateTime(dt);
        let json: serde_json::Value = (&ev).into();
        assert_eq!(
            json,
            serde_json::json!({
                "__drasi_v1_envelope__": "drasi.element_value.datetime",
                "__drasi_v1_type__": "drasi.LocalDateTime",
                "value": dt.to_string()
            })
        );
    }

    #[test]
    fn zoned_datetime_to_json() {
        let dt = sample_fixed_datetime();
        let ev = ElementValue::ZonedDateTime(dt);
        let json: serde_json::Value = (&ev).into();
        assert_eq!(
            json,
            serde_json::json!({
                "__drasi_v1_envelope__": "drasi.element_value.datetime",
                "__drasi_v1_type__": "drasi.ZonedDateTime",
                "value": dt.to_rfc3339()
            })
        );
    }

    // ── Round-trip: ElementValue → VariableValue → ElementValue ────────

    #[test]
    fn roundtrip_local_datetime() {
        let dt = sample_naive_datetime();
        let original = ElementValue::LocalDateTime(dt);
        let vv: VariableValue = (&original).into();
        let recovered: ElementValue = vv.try_into().unwrap();
        assert_eq!(original, recovered);
    }

    #[test]
    fn roundtrip_zoned_datetime() {
        let dt = sample_fixed_datetime();
        let original = ElementValue::ZonedDateTime(dt);
        let vv: VariableValue = (&original).into();
        let recovered: ElementValue = vv.try_into().unwrap();
        assert_eq!(original, recovered);
    }

    // ── JSON round-trip (note: JSON strings do NOT auto-parse to datetime) ──

    #[test]
    fn json_string_does_not_become_datetime() {
        // A JSON string that looks like a timestamp should remain an
        // ElementValue::String, not magically become LocalDateTime.
        let json = serde_json::Value::String("2024-06-15 10:30:45".to_string());
        let ev: ElementValue = (&json).into();
        assert!(matches!(ev, ElementValue::String(_)));
    }

    // ── JSON round-trip via tagged envelope ─────────────────────────────

    #[test]
    fn json_roundtrip_local_datetime() {
        let dt = sample_naive_datetime();
        let original = ElementValue::LocalDateTime(dt);
        let json: serde_json::Value = (&original).into();
        let recovered: ElementValue = (&json).into();
        assert_eq!(original, recovered);
    }

    #[test]
    fn json_roundtrip_zoned_datetime() {
        let dt = sample_fixed_datetime();
        let original = ElementValue::ZonedDateTime(dt);
        let json: serde_json::Value = (&original).into();
        let recovered: ElementValue = (&json).into();
        assert_eq!(original, recovered);
    }

    #[test]
    fn json_roundtrip_zoned_datetime_utc() {
        let dt = Utc
            .with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
            .unwrap()
            .fixed_offset();
        let original = ElementValue::ZonedDateTime(dt);
        let json: serde_json::Value = (&original).into();
        let recovered: ElementValue = (&json).into();
        assert_eq!(original, recovered);
    }

    #[test]
    fn tagged_object_with_unknown_type_stays_object() {
        // An object with the internal tag set to an unknown type should
        // be deserialized as a regular Object, not a datetime.
        let json = serde_json::json!({
            "__drasi_v1_envelope__": "drasi.element_value.datetime",
            "__drasi_v1_type__": "UnknownType",
            "value": "some-value"
        });
        let ev: ElementValue = (&json).into();
        assert!(matches!(ev, ElementValue::Object(_)));
    }

    #[test]
    fn tagged_object_with_invalid_datetime_stays_object() {
        // A tagged envelope with an unparseable value should fall back
        // to Object rather than panic.
        let json = serde_json::json!({
            "__drasi_v1_envelope__": "drasi.element_value.datetime",
            "__drasi_v1_type__": "drasi.ZonedDateTime",
            "value": "not-a-datetime"
        });
        let ev: ElementValue = (&json).into();
        assert!(matches!(ev, ElementValue::Object(_)));
    }

    #[test]
    fn object_with_common_type_key_does_not_get_interpreted_as_datetime() {
        // User objects using a common `_type` key must not collide with internal tags.
        let json = serde_json::json!({
            "_type": "drasi.LocalDateTime",
            "value": "2024-06-15 10:30:45",
            "timezone_name": "UTC"
        });
        let ev: ElementValue = (&json).into();
        assert!(matches!(ev, ElementValue::Object(_)));
    }

    #[test]
    fn object_with_internal_type_but_no_marker_stays_object() {
        // Objects that happen to contain the internal type key are not interpreted
        // as datetime envelopes unless the dedicated marker is also present.
        let json = serde_json::json!({
            "__drasi_v1_type__": "drasi.ZonedDateTime",
            "value": "1970-01-01T00:00:00+00:00"
        });
        let ev: ElementValue = (&json).into();
        assert!(matches!(ev, ElementValue::Object(_)));
    }

    // ── Other variants still work ──────────────────────────────────────

    #[test]
    fn null_roundtrip() {
        let ev = ElementValue::Null;
        let vv: VariableValue = (&ev).into();
        assert_eq!(vv, VariableValue::Null);
        let recovered: ElementValue = vv.try_into().unwrap();
        assert_eq!(recovered, ElementValue::Null);
    }

    #[test]
    fn bool_roundtrip() {
        let ev = ElementValue::Bool(true);
        let vv: VariableValue = (&ev).into();
        assert_eq!(vv, VariableValue::Bool(true));
    }

    #[test]
    fn integer_roundtrip() {
        let ev = ElementValue::Integer(42);
        let vv: VariableValue = (&ev).into();
        assert_eq!(vv, VariableValue::Integer(Integer::from(42i64)));
    }

    #[test]
    fn zoned_datetime_utc() {
        let dt = Utc
            .with_ymd_and_hms(2024, 1, 1, 0, 0, 0)
            .unwrap()
            .fixed_offset();
        let ev = ElementValue::ZonedDateTime(dt);
        let vv: VariableValue = (&ev).into();
        let recovered: ElementValue = vv.try_into().unwrap();
        assert_eq!(recovered, ev);
    }
}
