use std::collections::HashMap;

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

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

const DATA: &str = include_str!("../data.json");

#[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>,
}

#[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>,
}

#[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,
}

#[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 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,
}

#[derive(Debug)]
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,
                        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()
    }
}

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

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