use std::collections::{HashMap, HashSet};

use chrono::{prelude::Local, DateTime};
use serde::{Deserialize, Deserializer};
use tracing;

#[derive(Debug, Default, Deserialize)]
pub struct ExtraItemsPolicy {
    pub id: u32,
    pub count: u32,
    #[serde(default)]
    pub apply_on_owned_count: u32,
}

#[derive(Debug, Default, Deserialize)]
pub struct ProbabilityPoint {
    pub start_pity: u32,
    pub start_chance_percent: f64,
    #[serde(default)]
    pub increment_percent: f64,
}

#[derive(Debug, Default, Deserialize)]
pub struct ProbabilityModel {
    #[serde(default)]
    pub clear_status_on_higher_rarity_pulled: bool,
    pub points: Vec<ProbabilityPoint>,
    // This value is for display only, so it's set when
    // the maximum guarantee is not equal to the
    // automatically calculated value (commonly, less than).
    #[serde(default)]
    pub maximum_guarantee_pity: u32,

    #[serde(skip_deserializing)]
    probability_percents: Vec<f64>,
}

impl ProbabilityModel {
    fn get_maximum_guarantee(&self) -> u32 {
        self.probability_percents.len() as u32 - 1
    }

    pub fn post_configure(&mut self, tag: &String) {
        self.points.sort_by_key(|point| point.start_pity);

        let mut probability_percents: Vec<f64> = vec![0.0];
        for (i, point) in self.points.iter().enumerate() {
            if i > 0 {
                let last_point = &self.points[i - 1];
                let last_stop_percent = last_point.start_chance_percent
                    + last_point.increment_percent
                        * (point.start_pity - last_point.start_pity) as f64;
                if last_stop_percent > point.start_chance_percent {
                    tracing::warn!("Gacha - ProbabilityModel '{tag}': The start chance of '{point:?}' is less than the value inherited from the previous point.");
                }
            }

            let mut max_pity = 2000;
            if i < self.points.len() - 1 {
                let next_point = &self.points[i + 1];
                max_pity = next_point.start_pity - 1;
                let max_probability = point.start_chance_percent
                    + point.increment_percent
                        * (next_point.start_pity - 1 - point.start_pity) as f64;
                assert!(max_probability < 100.0, "Gacha - ProbabilityModel '{tag}': Probability already reached 100% in '{point:?}' (though points with higher pity left)");
            }

            let mut pity = point.start_pity;
            let mut percent = point.start_chance_percent;
            while pity <= max_pity {
                if max_pity >= 2000 && percent >= 100.0 {
                    probability_percents.push(100.0);
                    break;
                }
                probability_percents.push(percent);
                percent += point.increment_percent;
                pity += 1;
            }
            assert!(pity <= 2000, "Gacha - ProbabilityModel '{tag}' (point {i}): Haven't reached 100% guarantee probability at Pity 2001. The current probability is {percent}%. Crazy.");
        }

        self.probability_percents = probability_percents;
        if self.maximum_guarantee_pity <= 0 {
            self.maximum_guarantee_pity = self.get_maximum_guarantee();
        }
    }

    pub fn get_chance_percent(&self, pity: &u32) -> f64 {
        // The vec length is 1 bigger than the maximum pity (1-based)
        let guarantee_pity = self.probability_percents.len() - 1;
        let idx = *pity as usize;
        if idx > guarantee_pity {
            return self.probability_percents[guarantee_pity];
        }
        self.probability_percents[idx]
    }
}

#[allow(dead_code)]
#[derive(Debug, Default, Deserialize)]
pub struct CategoryGuaranteePolicy {
    pub included_category_tags: HashSet<String>,
    pub trigger_on_failure_times: u32,
    pub clear_status_on_target_changed: bool,
    pub chooseable: bool,
}

#[derive(Debug, Default, Deserialize)]
pub struct TenPullDiscount {
    pub use_limit: u32,
    pub discounted_prize: u32,
}

#[derive(Debug, Default, Deserialize)]
pub struct AdvancedGuarantee {
    pub use_limit: u32,
    pub rarity: u32,
    pub guarantee_pity: u32,
}

#[derive(Debug, Default, Deserialize)]
pub struct MustGainItem {
    pub use_limit: u32,
    pub rarity: u32,
    pub category_tag: String,
}

#[derive(Debug, Default, Deserialize)]
pub struct FreeSelectItem {
    pub milestones: Vec<u32>,
    pub rarity: u32,
    pub category_tags: Vec<String>,
    pub free_select_progress_record_tag: String,
    pub free_select_usage_record_tag: String,
}

#[derive(Debug, Default, Deserialize)]
pub struct DiscountPolicyCollection {
    pub ten_pull_discount_map: HashMap<String, TenPullDiscount>,
    pub must_gain_item_map: HashMap<String, MustGainItem>,
    pub advanced_guarantee_map: HashMap<String, AdvancedGuarantee>,
    pub free_select_map: HashMap<String, FreeSelectItem>,
}

impl DiscountPolicyCollection {
    pub fn post_configure(&mut self) {
        for (tag, ten_pull_discount) in self.ten_pull_discount_map.iter() {
            let discounted_prize = ten_pull_discount.discounted_prize;
            assert!(discounted_prize < 10, "Gacha - DiscountPolicy '{tag}': ten_pull_discount's value should be smaller than 10 (read {discounted_prize}).");
        }
    }
}

#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum GachaAddedItemType {
    #[default]
    None = 0,
    Weapon = 1,
    Character = 2,
    Bangboo = 3,
}

impl GachaAddedItemType {
    pub fn as_str_name(&self) -> &'static str {
        match self {
            GachaAddedItemType::None => "GACHA_ADDED_ITEM_TYPE_NONE",
            GachaAddedItemType::Weapon => "GACHA_ADDED_ITEM_TYPE_WEAPON",
            GachaAddedItemType::Character => "GACHA_ADDED_ITEM_TYPE_CHARACTER",
            GachaAddedItemType::Bangboo => "GACHA_ADDED_ITEM_TYPE_BANGBOO",
        }
    }
    pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
        match value {
            "GACHA_ADDED_ITEM_TYPE_NONE" => Some(Self::None),
            "GACHA_ADDED_ITEM_TYPE_WEAPON" => Some(Self::Weapon),
            "GACHA_ADDED_ITEM_TYPE_CHARACTER" => Some(Self::Character),
            "GACHA_ADDED_ITEM_TYPE_BANGBOO" => Some(Self::Bangboo),
            _ => None,
        }
    }
}

impl From<i32> for GachaAddedItemType {
    fn from(value: i32) -> Self {
        match value {
            1 => Self::Weapon,
            2 => Self::Character,
            3 => Self::Bangboo,
            _ => Self::None
        }
    }
}

impl Into<i32> for GachaAddedItemType {
    fn into(self) -> i32 {
        match self {
            Self::Weapon => 1,
            Self::Character => 2,
            Self::Bangboo => 3,
            Self::None => 0,
        }
    }
}

#[derive(Debug, Default, Deserialize)]
pub struct GachaCategoryInfo {
    #[serde(default)]
    pub is_promotional_items: bool,
    pub item_ids: Vec<u32>,
    pub category_weight: u32,
    #[serde(deserialize_with = "from_str")]
    pub item_type: GachaAddedItemType,
}

pub fn from_str<'de, D>(deserializer: D) -> Result<GachaAddedItemType, D::Error>
where
    D: Deserializer<'de>,
{
    let s: String = Deserialize::deserialize(deserializer)?;

    let result = GachaAddedItemType::from_str_name(&s);
    match result {
        Some(val) => Ok(val),
        None => Ok(GachaAddedItemType::None)
    }
}

#[derive(Debug, Default, Deserialize)]
pub struct GachaAvailableItemsInfo {
    pub rarity: u32,
    #[serde(default)]
    pub extra_items_policy_tags: Vec<String>,
    pub categories: HashMap<String, GachaCategoryInfo>,
    pub probability_model_tag: String,
    #[serde(default)]
    pub category_guarantee_policy_tags: Vec<String>,
}

#[allow(dead_code)]
#[derive(Debug, Default, Deserialize)]
pub struct CharacterGachaPool {
    pub gacha_schedule_id: u32,
    pub gacha_parent_schedule_id: u32,
    pub comment: String,
    pub gacha_type: u32,
    pub cost_item_id: u32,
    pub start_time: DateTime<Local>,
    pub end_time: DateTime<Local>,
    #[serde(default)]
    pub discount_policy_tags: Vec<String>,
    pub sharing_guarantee_info_category: String,
    pub gacha_items: Vec<GachaAvailableItemsInfo>,
}

impl CharacterGachaPool {
    pub fn is_still_open(&self, now: &DateTime<Local>) -> bool {
        self.start_time <= *now && *now <= self.end_time
    }

    pub fn post_configure(&mut self, probability_model_map: &HashMap<String, ProbabilityModel>) {
        self.gacha_items
            .sort_by_key(|item_list| u32::MAX - item_list.rarity);
        for items_info in self.gacha_items.iter_mut() {
            assert!(probability_model_map.contains_key(&items_info.probability_model_tag), "Gacha - CharacterGachaPool '{}': Specified ProbabilityModel tag '{}' that does not exist.", self.gacha_schedule_id, items_info.probability_model_tag);
        }
    }
}

#[derive(Debug, Default, Deserialize)]
pub struct GachaCommonProperties {
    pub up_item_category_tag: String,
    pub s_item_rarity: u32,
    pub a_item_rarity: u32,
    // TODO: PostConfigure check
    pub ten_pull_discount_tag: String,
    pub newcomer_advanced_s_tag: String,
}

#[derive(Debug, Default, Deserialize)]
pub struct GachaConfiguration {
    pub character_gacha_pool_list: Vec<CharacterGachaPool>,
    pub probability_model_map: HashMap<String, ProbabilityModel>,
    pub category_guarantee_policy_map: HashMap<String, CategoryGuaranteePolicy>,
    pub extra_items_policy_map: HashMap<String, ExtraItemsPolicy>,
    pub discount_policies: DiscountPolicyCollection,
    pub common_properties: GachaCommonProperties,
}

impl GachaConfiguration {
    pub fn post_configure(&mut self) {
        assert!(
            self.category_guarantee_policy_map
                .contains_key(&self.common_properties.up_item_category_tag),
            "The UP category should be valid in policy map."
        );

        for (tag, policy) in self.probability_model_map.iter_mut() {
            policy.post_configure(&tag);
        }
        self.discount_policies.post_configure();
        for character_pool in self.character_gacha_pool_list.iter_mut() {
            character_pool.post_configure(&self.probability_model_map);
        }
    }
}
