use crate::life_cycle_base::Impacts;
use crate::shared::{Conversion, MetaData, Reference, Source, Unit};
use chrono::NaiveDate;
use lcax_core::country::Country;
use lcax_core::dates::{deserialize_yyyy_mm_dd, serialize_yyyy_mm_dd};
use lcax_core::utils::get_version;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[cfg(feature = "jsbindings")]
use wasm_bindgen::prelude::*;

#[cfg(feature = "jsbindings")]
use tsify_next::Tsify;

#[cfg(feature = "jsbindings")]
use strum::IntoEnumIterator;

#[cfg(feature = "jsbindings")]
use strum_macros::EnumIter;

#[cfg(feature = "pybindings")]
use pyo3::exceptions::PyTypeError;
#[cfg(feature = "pybindings")]
use pyo3::prelude::*;
#[cfg(feature = "pybindings")]
use pyo3::types::PyType;

#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(
    feature = "jsbindings",
    derive(Tsify),
    tsify(into_wasm_abi, from_wasm_abi)
)]
#[cfg_attr(feature = "pybindings", pyclass(get_all, set_all))]
pub struct EPD {
    pub id: String,
    pub name: String,
    pub declared_unit: Unit,
    pub version: String,

    #[serde(serialize_with = "serialize_yyyy_mm_dd")]
    #[serde(deserialize_with = "deserialize_yyyy_mm_dd")]
    #[cfg_attr(feature = "jsbindings", tsify(type = "Date"))]
    pub published_date: NaiveDate,

    #[serde(serialize_with = "serialize_yyyy_mm_dd")]
    #[serde(deserialize_with = "deserialize_yyyy_mm_dd")]
    #[cfg_attr(feature = "jsbindings", tsify(type = "Date"))]
    pub valid_until: NaiveDate,

    pub source: Option<Source>,
    pub reference_service_life: Option<u32>,
    pub standard: Standard,
    pub comment: Option<String>,
    pub location: Country,
    pub subtype: SubType,
    pub conversions: Option<Vec<Conversion>>,
    pub impacts: Impacts,
    pub meta_data: Option<MetaData>,
}

impl Default for EPD {
    fn default() -> Self {
        Self {
            id: uuid::Uuid::new_v4().to_string(),
            name: "".to_string(),
            declared_unit: Unit::UNKNOWN,
            version: get_version(),
            published_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
            valid_until: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
            source: None,
            reference_service_life: None,
            standard: Standard::UNKNOWN,
            comment: None,
            location: Country::UNKNOWN,
            subtype: SubType::GENERIC,
            conversions: None,
            impacts: Impacts::new(),
            meta_data: None,
        }
    }
}

#[cfg_attr(feature = "pybindings", pymethods)]
impl EPD {
    #[cfg(feature = "pybindings")]
    #[new]
    #[pyo3(signature=(name, declared_unit, version, published_date, valid_until, standard, location, subtype, impacts, id=None, source=None, reference_service_life=None, comment=None, conversions=None, meta_data=None ))]
    pub fn new_py(
        name: String,
        declared_unit: Unit,
        version: String,
        published_date: NaiveDate,
        valid_until: NaiveDate,
        standard: Standard,
        location: Country,
        subtype: SubType,
        impacts: Impacts,
        id: Option<String>,
        source: Option<Source>,
        reference_service_life: Option<u32>,
        comment: Option<String>,
        conversions: Option<Vec<Conversion>>,
        meta_data: Option<MetaData>,
    ) -> Self {
        let _id = id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        Self {
            id: _id,
            name,
            declared_unit,
            version,
            published_date,
            valid_until,
            source,
            reference_service_life,
            standard,
            comment,
            location,
            subtype,
            conversions,
            impacts,
            meta_data,
        }
    }

    #[cfg(feature = "pybindings")]
    #[classmethod]
    #[pyo3(name = "loads")]
    pub fn loads_py(_cls: &Bound<'_, PyType>, value: &str) -> PyResult<Self> {
        match EPD::loads(value) {
            Ok(epd) => Ok(epd),
            Err(error) => Err(PyTypeError::new_err(error.to_string())),
        }
    }

    #[cfg(feature = "pybindings")]
    #[pyo3(name = "dumps")]
    pub fn dumps_py(&self) -> PyResult<String> {
        match EPD::dumps(self) {
            Ok(data) => Ok(data),
            Err(error) => Err(PyTypeError::new_err(error.to_string())),
        }
    }

    #[cfg(feature = "pybindings")]
    fn __repr__(&self) -> String {
        format!("EPD: {}", self.id)
    }

    #[cfg(feature = "pybindings")]
    fn __str__(&self) -> String {
        format!("EPD: {}", self.id)
    }
}

impl EPD {
    pub fn new(
        id: Option<String>,
        name: String,
        declared_unit: Unit,
        version: String,
        published_date: NaiveDate,
        valid_until: NaiveDate,
        source: Option<Source>,
        reference_service_life: Option<u32>,
        standard: Standard,
        comment: Option<String>,
        location: Country,
        subtype: SubType,
        conversions: Option<Vec<Conversion>>,
        impacts: Impacts,
        meta_data: Option<MetaData>,
    ) -> Self {
        let _id = id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        Self {
            id: _id,
            name,
            declared_unit,
            version,
            published_date,
            valid_until,
            source,
            reference_service_life,
            standard,
            comment,
            location,
            subtype,
            conversions,
            impacts,
            meta_data,
        }
    }

    pub fn loads(value: &str) -> Result<EPD, serde_json::Error> {
        serde_json::from_str(value)
    }

    pub fn dumps(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string(self)
    }
}

#[derive(Deserialize, Serialize, JsonSchema, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(
    feature = "jsbindings",
    derive(Tsify, EnumIter),
    tsify(into_wasm_abi, from_wasm_abi)
)]
#[cfg_attr(feature = "pybindings", pyclass(eq, eq_int))]
pub enum Standard {
    EN15804A1,
    EN15804A2,
    UNKNOWN,
}

#[cfg(feature = "jsbindings")]
#[allow(non_snake_case)]
#[wasm_bindgen]
pub fn standards() -> Vec<Standard> {
    Standard::iter().collect()
}

impl From<&String> for Standard {
    fn from(value: &String) -> Self {
        if value.to_ascii_lowercase().contains("15804+a2") {
            Standard::EN15804A2
        } else if value.to_ascii_lowercase().contains("15804") {
            Standard::EN15804A1
        } else {
            Standard::UNKNOWN
        }
    }
}

#[derive(Deserialize, Serialize, JsonSchema, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(
    feature = "jsbindings",
    derive(Tsify, EnumIter),
    tsify(into_wasm_abi, from_wasm_abi)
)]
#[cfg_attr(feature = "pybindings", pyclass(eq, eq_int))]
pub enum SubType {
    GENERIC,
    SPECIFIC,
    INDUSTRY,
    REPRESENTATIVE,
}

#[cfg(feature = "jsbindings")]
#[allow(non_snake_case)]
#[wasm_bindgen]
pub fn subTypes() -> Vec<SubType> {
    SubType::iter().collect()
}

impl From<&Option<String>> for SubType {
    fn from(value: &Option<String>) -> Self {
        match value {
            Some(_value) if _value.to_lowercase().contains("representative") => {
                SubType::REPRESENTATIVE
            }
            Some(_value) if _value.to_lowercase().contains("specific") => SubType::SPECIFIC,
            Some(_value) if _value.to_lowercase().contains("industry") => SubType::INDUSTRY,
            _ => SubType::GENERIC,
        }
    }
}

impl Into<String> for SubType {
    fn into(self) -> String {
        match self {
            SubType::REPRESENTATIVE => "representative".to_string(),
            SubType::SPECIFIC => "specific".to_string(),
            SubType::INDUSTRY => "industry".to_string(),
            SubType::GENERIC => "generic".to_string(),
        }
    }
}

#[derive(Deserialize, Serialize, JsonSchema, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type")]
#[cfg_attr(feature = "jsbindings", derive(Tsify))]
#[cfg_attr(feature = "pybindings", derive(FromPyObject, IntoPyObject))]
pub enum EPDReference {
    #[serde(rename = "EPD")]
    EPD(EPD),
    Reference(Reference),
}

impl EPDReference {
    pub fn resolve(&self) -> Result<&EPD, String> {
        match self {
            EPDReference::EPD(epd) => Ok(epd),
            _ => Err("Handling of references not implemented yet!".to_string()),
        }
    }

    pub fn new(
        _type: &str,
        id: Option<String>,
        name: String,
        declared_unit: Unit,
        version: String,
        published_date: NaiveDate,
        valid_until: NaiveDate,
        source: Option<Source>,
        reference_service_life: Option<u32>,
        standard: Standard,
        comment: Option<String>,
        location: Country,
        subtype: SubType,
        conversions: Option<Vec<Conversion>>,
        impacts: Impacts,
        meta_data: Option<MetaData>,
    ) -> Self {
        match _type {
            "epd" => EPDReference::EPD(EPD::new(
                id,
                name,
                declared_unit,
                version,
                published_date,
                valid_until,
                source,
                reference_service_life,
                standard,
                comment,
                location,
                subtype,
                conversions,
                impacts,
                meta_data,
            )),
            // "reference" => EPDReference::Reference(Reference::new()),
            &_ => panic!("Unknown impact type!"),
        }
    }
}
