use core::fmt;
use std::{collections::HashMap, fmt::Display};

use chrono::{DateTime, Datelike, Duration, Timelike, Utc};
use chrono_humanize::HumanTime;
use serde::{Deserialize, Serialize};

use crate::{
    clock,
    forecast::{round_to_last_weather_time, Forecast, ForecastSet, Rate},
    FilterQuery,
};

const DATA: &str = include_str!("../data.json");
pub const PATCHES: [f32; 32] = [
    2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 5.0,
    5.1, 5.2, 5.3, 5.4, 5.5, 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 7.0, 7.1,
];

#[derive(Serialize, Deserialize, Debug)]
pub struct Data {
    pub db_data: SubData,
    pub fish_entries: Vec<FishMeta>,
    #[serde(skip)]
    pub forecasts: ForecastSet,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct SubData {
    #[serde(alias = "FISH")]
    pub fish: HashMap<u32, FishEntry>,
    #[serde(alias = "ZONES")]
    pub zones: HashMap<u32, Zone>,
    #[serde(alias = "WEATHER_RATES")]
    pub weather_rates: HashMap<u32, WeatherRate>,
    #[serde(alias = "FISHING_SPOTS")]
    pub fishing_spots: HashMap<u32, FishingSpot>,
    #[serde(alias = "WEATHER_TYPES")]
    pub weather_types: HashMap<u32, WeatherType>,
    #[serde(alias = "ITEMS")]
    pub items: HashMap<u32, Item>,
}

#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct FishEntry {
    #[serde(alias = "_id")]
    pub id: u32,
    pub start_hour: Option<f32>,
    pub end_hour: Option<f32>,
    pub location: Option<u32>,
    pub best_catch_path: Vec<u32>,
    pub predators: Vec<Vec<u32>>,
    pub intuition_length: Option<u32>,
    pub patch: f32,
    pub folklore: Option<u32>,
    pub fish_eyes: bool,
    pub big_fish: bool,
    pub weather_set: Vec<u32>,
    pub previous_weather_set: Vec<u32>,
    pub hookset: Option<Hookset>,
    pub tug: Option<Tug>,
}

#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)]
pub enum Hookset {
    #[default]
    Precision,
    Powerful,
}

impl Display for Hookset {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
        let string = match self {
            Self::Precision => "/static/precision.png",
            Self::Powerful => "/static/powerful.png",
        };
        write!(f, "{}", string)
    }
}

impl Display for Tug {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Light => write!(f, "!"),
            Self::Medium => write!(f, "!!"),
            Self::Heavy => write!(f, "!!!"),
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum Tug {
    #[default]
    Light,
    Medium,
    Heavy,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Item {
    #[serde(alias = "_id")]
    pub id: u32,
    pub name_en: String,
    pub icon: String,
}

impl Item {
    pub fn get_icon_url(&self) -> String {
        format!("/static/icons/{}.png", self.icon)
    }

    pub fn get_hookset(&self, data: &Data) -> Option<Hookset> {
        data.db_data.fish.get(&self.id).and_then(|f| f.hookset)
    }

    pub fn get_tug(&self, data: &Data) -> Option<Tug> {
        data.db_data.fish.get(&self.id).and_then(|f| f.tug)
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Zone {
    pub name_en: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct FishingSpot {
    #[serde(alias = "_id")]
    pub id: u32,
    pub name_en: String,
    #[serde(alias = "territory_id")] // not sure why it's named this, but thanks serde!
    pub forecast_id: u32,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct WeatherRate {
    pub zone_id: u32,
    pub weather_rates: Vec<Vec<u32>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct WeatherType {
    pub name_en: String,
    pub icon: String,
}

impl WeatherType {
    pub fn get_icon_url(&self) -> String {
        let mut icon_cat = self.icon.clone();
        icon_cat.replace_range(3..6, "000");
        format!(
            "https://v2.xivapi.com/api/asset?path=ui/icon/{}/{}.tex&format=png",
            icon_cat, self.icon
        )
    }
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Location {
    pub id: u32,
    pub name_en: String,
    pub map_coords: (f32, f32, u32),
}

#[derive(Serialize, Deserialize, Debug, Default)]
pub struct FishMeta {
    pub id: u32,
    pub name_en: String,
    pub region_en: String,
    pub zone_en: String,
}

#[derive(Clone)]
pub struct CombinedFish<'a> {
    pub entry: &'a FishEntry,
    pub meta: &'a FishMeta,
    pub is_up: bool,
    pub is_always_up: bool,
    pub windows: Vec<Window>,
    pub rarity: f32,
    pub filtered: bool,
}

#[derive(Debug, Clone, Copy)]
pub struct Window {
    pub start_time: DateTime<Utc>,
    pub duration: Duration,
}

impl Window {
    pub fn display_end_time(&self) -> String {
        let end_date = self.start_time + self.duration;
        let human_date = HumanTime::from(clock::to_earth_time(end_date));
        format!("{}", human_date)
    }

    pub fn display_start_time(&self) -> String {
        let human_date = HumanTime::from(clock::to_earth_time(self.start_time));
        format!("{}", human_date)
    }
}

#[allow(clippy::needless_lifetimes)]
impl<'a> CombinedFish<'a> {
    /// Fills in the rest of the struct.
    pub fn find_uptime(&mut self, data: &Data) {
        // If the start and end times are equal, and there's no weather constraint, the fish is always up.
        if self
            .entry
            .start_hour
            .is_some_and(|f| self.entry.end_hour.is_some_and(|g| f == 0. && g == 24.))
            && self.entry.weather_set.is_empty()
        {
            self.is_always_up = true;
            return;
        }
        self.windows = self.get_next_uptimes(data);
        let now = clock::get_current_eorzea_date();
        let first_window = self.windows.first();
        self.is_up = if let Some(window) = first_window {
            (window.start_time < now) && ((window.start_time + window.duration) > now)
        } else {
            false
        };

        // Find the rarity of the fish. We define this as the percentage of uptime in the next 10 days.
        let duration = clock::to_eorzea_time(Utc::now() + Duration::days(10))
            - clock::get_current_eorzea_date();
        let up_durations = self
            .windows
            .iter()
            .fold(Duration::zero(), |acc, w| acc + w.duration);
        self.rarity = up_durations.num_minutes() as f32 / duration.num_minutes() as f32;
    }

    /// Finds out whether the fish is technically up at the given time (discounting factors like weather).
    pub fn is_in_time_range_at(&self, date: DateTime<Utc>) -> bool {
        if self.entry.start_hour.is_none() || self.entry.end_hour.is_none() {
            return false;
        }
        let et = clock::to_eorzea_time(date);
        let start_hour = self.entry.start_hour.unwrap();
        let end_hour = self.entry.end_hour.unwrap();
        let spans_midnight = start_hour > end_hour;
        let cur_hour = et.hour() as f32 + et.minute() as f32 / 60.;
        if spans_midnight {
            cur_hour > start_hour || cur_hour < end_hour
        } else {
            start_hour < cur_hour && cur_hour < end_hour
        }
    }

    /// Gets the associated forecast struct.
    pub fn get_forecast<'b>(&self, data: &'b Data) -> Option<&'b Forecast> {
        self.entry.location?;
        // Find the associated fishing spot.
        let spot = data
            .db_data
            .fishing_spots
            .get(&self.entry.location.unwrap())
            .unwrap();
        data.forecasts.get(&spot.forecast_id)
    }

    /// Finds out whether the fish is in favorable weather conditions at a specific Eorzean time.
    pub fn is_in_correct_weather_at(&self, forecast: &Forecast, date: DateTime<Utc>) -> bool {
        if self.entry.weather_set.is_empty() {
            return true;
        }
        // Check if the current weather is right!
        let cur_weather = forecast.weather_at(date);
        let is_current = self
            .entry
            .weather_set
            .iter()
            .any(|ws| ws == &cur_weather.weather_id);

        // Check if the fish depends on a previous weather, and if so, if that weather is
        // right as well.
        let is_past = if self.entry.previous_weather_set.is_empty() {
            true
        } else {
            let prev_date = date - Duration::hours(8);
            let prev_weather = forecast.weather_at(prev_date);
            self.entry
                .previous_weather_set
                .iter()
                .any(|ws| ws == &prev_weather.weather_id)
        };

        is_current && is_past
    }

    // Given a forecast and a start date of a window, find how long that weather stays.
    pub fn get_weather_duration(&self, forecast: &Forecast, date: &DateTime<Utc>) -> Duration {
        let mut length = 0;
        #[allow(clippy::clone_on_copy)]
        let mut date = date.clone();
        let mut cur_pattern = forecast.weather_at(date).weather_id;
        while self.entry.weather_set.iter().any(|w| w == &cur_pattern) {
            length += 8;
            date += Duration::hours(8);
            cur_pattern = forecast.weather_at(date).weather_id;
        }
        Duration::hours(length)
    }

    // Calculate the uptime windows occurring in the next 10 days.
    pub fn get_next_uptimes(&self, data: &Data) -> Vec<Window> {
        let mut idx_date = round_to_last_weather_time(clock::get_current_eorzea_date());
        let goal_date = clock::to_eorzea_time(clock::to_earth_time(idx_date) + Duration::days(10));

        // If it's a time-only fish, the procedure is a bit simpler.
        if self.entry.weather_set.is_empty() && self.entry.start_hour.is_some() {
            let mut idx_date = clock::get_current_eorzea_date(); // We don't need to round to a weather time here.
            let mut results = Vec::new();
            let start_hour = self.entry.start_hour.unwrap();
            let end_hour = self.entry.end_hour.unwrap();
            while idx_date < goal_date {
                // Because there will be a window every single day, just set our clock to when it starts.
                let next_start =
                    clock::set_hm_from_float(&idx_date, self.entry.start_hour.unwrap());
                let duration = clock::get_window_duration(start_hour, end_hour);
                // Safely push the full duration.
                results.push(Window {
                    start_time: next_start,
                    duration,
                });
                // If the end time is on the same day, push the clock ahead by a day so that we don't use the same
                // window the next iteration.
                let next_end = next_start + duration;
                if next_start.day() == next_end.day() {
                    idx_date += Duration::days(1);
                } else {
                    idx_date = next_end;
                }
            }
            return results;
        }

        // If our fish is weather-affected, we need to be a bit more careful.
        let forecast = self.get_forecast(data).unwrap();
        let has_time_constraint =
            !(self.entry.start_hour.unwrap() == 0. && self.entry.end_hour.unwrap() == 24.);
        let mut results = Vec::new();
        // No use in running this loop if the fish is up all the time.
        if !has_time_constraint && self.entry.weather_set.is_empty() {
            return results;
        }
        while idx_date < goal_date {
            // Is our fish technically in favorable weather here?
            let has_weather = self.is_in_correct_weather_at(forecast, idx_date);
            // If we don't have a time constraint, and the weather is good, add to our results.

            if !has_time_constraint && has_weather {
                // The weather might drag on for more than one cycle, find out when it ends.
                // However: A weather constraint with a previous weather set will never have two consecutive uptime cycles.
                let duration = if self.entry.previous_weather_set.is_empty() {
                    self.get_weather_duration(forecast, &idx_date)
                } else {
                    Duration::hours(8)
                };
                results.push(Window {
                    start_time: idx_date,
                    duration,
                });

                idx_date += duration;
            } else if has_time_constraint && has_weather {
                // Calculate start and end times for both the weather uptime and the time-based uptime.
                let w_duration = if self.entry.previous_weather_set.is_empty() {
                    self.get_weather_duration(forecast, &idx_date)
                } else {
                    Duration::hours(8)
                };
                let w_start_date = idx_date;
                let w_end_date = w_start_date + w_duration;
                let start_hour = self.entry.start_hour.unwrap();
                let end_hour = self.entry.end_hour.unwrap();
                // If the current weather cycle is the midnight one, and the timespan wraps a day, start
                // at the previous day.
                let t_start_date = if idx_date.hour() == 0 && start_hour > end_hour {
                    clock::set_hm_from_float(
                        &(idx_date - Duration::days(1)),
                        self.entry.start_hour.unwrap(),
                    )
                } else {
                    clock::set_hm_from_float(&idx_date, self.entry.start_hour.unwrap())
                };
                let t_duration = clock::get_window_duration(
                    self.entry.start_hour.unwrap(),
                    self.entry.end_hour.unwrap(),
                );
                let t_end_date = t_start_date + t_duration;
                // We have to check four cases:
                if t_start_date <= w_start_date && t_end_date >= w_end_date {
                    // CASE 1: The time-based uptime exceeds the weather-based uptime at start and end
                    // This is the easiest: Just save the full weather-based uptime.
                    results.push(Window {
                        start_time: w_start_date,
                        duration: w_duration,
                    });
                } else if w_start_date <= t_start_date && w_end_date >= t_end_date {
                    // CASE 2: Vice-versa, the weather-based uptime exceeds the time-based uptime.
                    // Also just save the time-based uptime.
                    results.push(Window {
                        start_time: t_start_date,
                        duration: t_duration,
                    })
                } else if w_start_date <= t_start_date
                    && t_end_date >= w_end_date
                    && w_end_date > t_start_date
                {
                    // CASE 3: The weather starts before the time, but the doesn't last beyond it.
                    // In this case, save the time-based start time, and the duration between time-based start time
                    // and weather-based end time.
                    let diff = w_end_date - t_start_date;
                    results.push(Window {
                        start_time: t_start_date,
                        duration: diff,
                    });
                } else if t_start_date <= w_start_date
                    && w_end_date >= t_end_date
                    && t_end_date > w_start_date
                {
                    // CASE 4: The time starts before the weather, but doesn't last beyond it.
                    // Do the opposite of Case 3 here.
                    let diff = t_end_date - w_start_date;
                    results.push(Window {
                        start_time: w_start_date,
                        duration: diff,
                    })
                }

                idx_date += w_duration;
            } else {
                idx_date += Duration::hours(8);
            }
        }
        results
    }
}

#[allow(clippy::new_without_default)]
impl Data {
    pub fn new() -> Self {
        let json = &mut serde_json::Deserializer::from_str(DATA);
        let mut data: Self = serde_path_to_error::deserialize(json).unwrap();
        data.forecasts = data
            .db_data
            .weather_rates
            .iter()
            .map(|(id, wr)| {
                (
                    *id,
                    Forecast {
                        zone_id: wr.zone_id,
                        rates: wr
                            .weather_rates
                            .iter()
                            .map(|rate| Rate {
                                weather_id: rate[0],
                                rate: rate[1],
                            })
                            .collect(),
                    },
                )
            })
            .collect();
        data
    }

    pub fn fish_with_meta(&self) -> HashMap<u32, CombinedFish> {
        self.db_data
            .fish
            .iter()
            .filter_map(|(k, v)| {
                let corresponding_meta = self.fish_entries.iter().find(|m| &m.id == k);
                corresponding_meta.map(|m| {
                    let mut cfish = CombinedFish {
                        entry: v,
                        meta: m,
                        filtered: false,
                        is_up: false,        // fake default values for now
                        is_always_up: false, // dito
                        windows: Vec::new(), // dito
                        rarity: 1.,          // dito
                    };
                    cfish.find_uptime(self);
                    (*k, cfish)
                })
            })
            .collect()
    }
}

#[derive(Debug, Clone)]
pub struct Filters {
    /// Display caught fish.
    pub include_caught: bool,
    /// Display non-big fish.
    pub non_big_fish: bool,
    /// Filter by patch.
    pub patches: Vec<f32>,
}

impl Filters {
    pub fn from_query(query: FilterQuery) -> Self {
        Self {
            // As caught fish are filtered by default, setting the query parameter un-filters them.
            include_caught: query.caught.is_some(),
            non_big_fish: query.big.is_some(),
            patches: query.patches.unwrap_or_default(),
        }
    }

    #[allow(elided_named_lifetimes)]
    pub fn filter<'a>(
        &'a self,
        fish: Vec<&'a CombinedFish>,
        caught_fish_ids: &[i32],
    ) -> Vec<CombinedFish> {
        fish.into_iter()
            .cloned()
            .map(|fish| {
                let f_caught = if self.include_caught {
                    true
                } else {
                    !caught_fish_ids.contains(&(fish.entry.id as i32))
                };

                let f_big = if self.non_big_fish {
                    true
                } else {
                    fish.entry.big_fish
                };

                let f_patch = if !self.patches.is_empty() {
                    self.patches.contains(&fish.entry.patch)
                } else {
                    true
                };

                CombinedFish {
                    filtered: !(f_caught && f_patch && f_big),
                    ..fish
                }
            })
            .collect()
    }
}

pub fn get_weather_name(data: &Data, id: &u32) -> String {
    data.db_data.weather_types.get(id).unwrap().name_en.clone()
}

pub fn get_weather_icon(data: &Data, id: &u32) -> String {
    data.db_data.weather_types.get(id).unwrap().get_icon_url()
}

pub fn get_zone_name(data: &Data, id: u32) -> &str {
    &data.db_data.zones.get(&id).unwrap().name_en
}

pub fn display_intuition_length(length: u32) -> String {
    let dur = Duration::seconds(length as i64);
    format!("{}m{}s", dur.num_minutes(), dur.num_seconds() % 60)
}
