/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of FitnessTrax.

FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/

use crate::app::{ReadError, RecordProvider};
use dimensioned::si;
use emseries::{Record, RecordId, Recordable};
use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord};
use std::{
    collections::HashMap,
    ops::Deref,
    sync::{Arc, RwLock},
};

// These are actually a used imports. Clippy isn't detecting their use, probably because of complexity around the async trait macros.
#[allow(unused_imports)]
use crate::app::WriteError;
#[allow(unused_imports)]
use chrono::NaiveDate;

#[derive(Clone, Debug)]
enum RecordState<T: Clone + Recordable> {
    Original(Record<T>),
    New(Record<T>),
    Updated(Record<T>),
    Deleted(Record<T>),
}

impl<T: Clone + emseries::Recordable> RecordState<T> {
    fn exists(&self) -> bool {
        match self {
            RecordState::Original(_) => true,
            RecordState::New(_) => true,
            RecordState::Updated(_) => true,
            RecordState::Deleted(_) => false,
        }
    }

    #[allow(unused)]
    fn data(&self) -> Option<&Record<T>> {
        match self {
            RecordState::Original(ref r) => Some(r),
            RecordState::New(ref r) => None,
            RecordState::Updated(ref r) => Some(r),
            RecordState::Deleted(ref r) => Some(r),
        }
    }

    fn set_value(&mut self, value: T) {
        *self = match self {
            RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }),
            RecordState::New(r) => RecordState::New(Record { data: value, ..*r }),
            RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }),
            RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }),
        };
    }

    fn with_value(mut self, value: T) -> RecordState<T> {
        self.set_value(value);
        self
    }

    #[allow(unused)]
    fn with_delete(self) -> Option<RecordState<T>> {
        match self {
            RecordState::Original(r) => Some(RecordState::Deleted(r)),
            RecordState::New(r) => None,
            RecordState::Updated(r) => Some(RecordState::Deleted(r)),
            RecordState::Deleted(r) => Some(RecordState::Deleted(r)),
        }
    }
}

impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        match self {
            RecordState::Original(ref r) => &r.data,
            RecordState::New(ref r) => &r.data,
            RecordState::Updated(ref r) => &r.data,
            RecordState::Deleted(ref r) => &r.data,
        }
    }
}

impl<T: Clone + emseries::Recordable> std::ops::DerefMut for RecordState<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        match self {
            RecordState::Original(ref mut r) => &mut r.data,
            RecordState::New(ref mut r) => &mut r.data,
            RecordState::Updated(ref mut r) => &mut r.data,
            RecordState::Deleted(ref mut r) => &mut r.data,
        }
    }
}

#[derive(Clone)]
pub struct DayDetailViewModel {
    provider: Arc<dyn RecordProvider>,
    pub date: chrono::NaiveDate,
    weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
    steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
    records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>,
}

impl DayDetailViewModel {
    pub async fn new(
        date: chrono::NaiveDate,
        provider: impl RecordProvider + 'static,
    ) -> Result<Self, ReadError> {
        let s = Self {
            provider: Arc::new(provider),
            date,
            weight: Arc::new(RwLock::new(None)),
            steps: Arc::new(RwLock::new(None)),
            records: Arc::new(RwLock::new(HashMap::new())),
        };
        s.populate_records().await;
        Ok(s)
    }

    pub fn weight(&self) -> Option<si::Kilogram<f64>> {
        (*self.weight.read().unwrap()).as_ref().map(|w| w.weight)
    }

    pub fn set_weight(&self, new_weight: si::Kilogram<f64>) {
        let mut record = self.weight.write().unwrap();
        let new_record = match *record {
            Some(ref rstate) => rstate.clone().with_value(ft_core::Weight {
                date: self.date,
                weight: new_weight,
            }),
            None => RecordState::New(Record {
                id: RecordId::default(),
                data: ft_core::Weight {
                    date: self.date,
                    weight: new_weight,
                },
            }),
        };
        *record = Some(new_record);
    }

    pub fn steps(&self) -> Option<u32> {
        (*self.steps.read().unwrap()).as_ref().map(|w| w.count)
    }

    pub fn set_steps(&self, new_count: u32) {
        let mut record = self.steps.write().unwrap();
        let new_record = match *record {
            Some(ref rstate) => rstate.clone().with_value(ft_core::Steps {
                date: self.date,
                count: new_count,
            }),
            None => RecordState::New(Record {
                id: RecordId::default(),
                data: ft_core::Steps {
                    date: self.date,
                    count: new_count,
                },
            }),
        };
        *record = Some(new_record);
    }

    pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record<TimeDistance> {
        let now = chrono::Local::now();
        let base_time = now.time();
        let tz = now.timezone();
        let datetime = self
            .date
            .clone()
            .and_time(base_time)
            .and_local_timezone(tz)
            .unwrap()
            .into();

        let id = RecordId::default();
        let workout = TimeDistance {
            datetime,
            activity,
            distance: None,
            duration: None,
            comments: None,
        };
        let tr = TraxRecord::from(workout.clone());
        self.records
            .write()
            .unwrap()
            .insert(id, RecordState::New(Record { id, data: tr }));
        Record { id, data: workout }
    }

    pub fn time_distance_records(&self) -> Vec<Record<TimeDistance>> {
        self.records
            .read()
            .unwrap()
            .iter()
            .filter(|(_, record)| record.exists())
            .filter_map(|(id, record_state)| match **record_state {
                TraxRecord::TimeDistance(ref workout) => Some(Record {
                    id: *id,
                    data: workout.clone(),
                }),
                _ => None,
            })
            .collect()
    }

    pub fn time_distance_summary(
        &self,
        activity: TimeDistanceActivity,
    ) -> (si::Meter<f64>, si::Second<f64>) {
        self.time_distance_records()
            .into_iter()
            .filter(|rec| rec.data.activity == activity)
            .fold(
                (0. * si::M, 0. * si::S),
                |(distance, duration), workout| match (workout.data.distance, workout.data.duration)
                {
                    (Some(distance_), Some(duration_)) => {
                        (distance + distance_, duration + duration_)
                    }
                    (Some(distance_), None) => (distance + distance_, duration),
                    (None, Some(duration_)) => (distance, duration + duration_),
                    (None, None) => (distance, duration),
                },
            )
    }

    pub fn update_record(&self, update: Record<TraxRecord>) {
        let mut records = self.records.write().unwrap();
        records
            .entry(update.id)
            .and_modify(|record| record.set_value(update.data));
    }

    pub fn records(&self) -> Vec<Record<TraxRecord>> {
        let read_lock = self.records.read().unwrap();
        read_lock
            .iter()
            .filter_map(|(_, record_state)| record_state.data())
            .cloned()
            .collect::<Vec<Record<TraxRecord>>>()
    }

    #[allow(unused)]
    fn get_record(&self, id: &RecordId) -> Option<Record<TraxRecord>> {
        let record_set = self.records.read().unwrap();
        record_set.get(id).map(|record| Record {
            id: *id,
            data: (**record).clone(),
        })
    }

    pub fn remove_record(&self, id: RecordId) {
        let mut record_set = self.records.write().unwrap();
        let updated_record = match record_set.remove(&id) {
            Some(RecordState::Original(r)) => Some(RecordState::Deleted(r)),
            Some(RecordState::New(_)) => None,
            Some(RecordState::Updated(r)) => Some(RecordState::Deleted(r)),
            Some(RecordState::Deleted(r)) => Some(RecordState::Deleted(r)),
            None => None,
        };
        if let Some(updated_record) = updated_record {
            record_set.insert(id, updated_record);
        }
    }

    pub fn save(&self) {
        let s = self.clone();

        glib::spawn_future(async move { s.async_save().await });
    }

    pub async fn async_save(&self) {
        let weight_record = self.weight.read().unwrap().clone();
        match weight_record {
            Some(RecordState::New(data)) => {
                let _ = self
                    .provider
                    .put_record(TraxRecord::Weight(data.data))
                    .await;
            }
            Some(RecordState::Original(_)) => {}
            Some(RecordState::Updated(weight)) => {
                let _ = self
                    .provider
                    .update_record(Record {
                        id: weight.id,
                        data: TraxRecord::Weight(weight.data),
                    })
                    .await;
            }
            Some(RecordState::Deleted(_)) => {}
            None => {}
        }

        let steps_record = self.steps.read().unwrap().clone();
        match steps_record {
            Some(RecordState::New(data)) => {
                let _ = self.provider.put_record(TraxRecord::Steps(data.data)).await;
            }
            Some(RecordState::Original(_)) => {}
            Some(RecordState::Updated(steps)) => {
                let _ = self
                    .provider
                    .update_record(Record {
                        id: steps.id,
                        data: TraxRecord::Steps(steps.data),
                    })
                    .await;
            }
            Some(RecordState::Deleted(_)) => {}
            None => {}
        }

        let records = self
            .records
            .write()
            .unwrap()
            .drain()
            .map(|(_, record)| record)
            .collect::<Vec<RecordState<TraxRecord>>>();

        for record in records {
            println!("saving record: {:?}", record);
            match record {
                RecordState::New(data) => {
                    let _ = self.provider.put_record(data.data).await;
                }
                RecordState::Original(_) => {}
                RecordState::Updated(r) => {
                    let _ = self.provider.update_record(r.clone()).await;
                }
                RecordState::Deleted(r) => {
                    let _ = self.provider.delete_record(r.id).await;
                }
            }
        }
        self.populate_records().await;
    }

    pub async fn revert(&self) {
        self.populate_records().await;
    }

    async fn populate_records(&self) {
        let records = self.provider.records(self.date, self.date).await.unwrap();

        let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
            records.into_iter().partition(|r| r.data.is_weight());
        let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
            records.into_iter().partition(|r| r.data.is_steps());

        *self.weight.write().unwrap() = weight_records
            .first()
            .and_then(|r| match r.data {
                TraxRecord::Weight(ref w) => Some((r.id, w.clone())),
                _ => None,
            })
            .map(|(id, w)| RecordState::Original(Record { id, data: w }));

        *self.steps.write().unwrap() = step_records
            .first()
            .and_then(|r| match r.data {
                TraxRecord::Steps(ref w) => Some((r.id, w.clone())),
                _ => None,
            })
            .map(|(id, w)| RecordState::Original(Record { id, data: w }));

        *self.records.write().unwrap() = records
            .into_iter()
            .map(|r| (r.id, RecordState::Original(r)))
            .collect::<HashMap<RecordId, RecordState<TraxRecord>>>();
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use async_trait::async_trait;
    use chrono::{DateTime, FixedOffset};
    use dimensioned::si;
    use emseries::Record;

    #[derive(Clone, Debug)]
    struct MockProvider {
        records: Arc<RwLock<HashMap<RecordId, Record<TraxRecord>>>>,

        put_records: Arc<RwLock<Vec<Record<TraxRecord>>>>,
        updated_records: Arc<RwLock<Vec<Record<TraxRecord>>>>,
        deleted_records: Arc<RwLock<Vec<RecordId>>>,
    }

    impl MockProvider {
        fn new(records: Vec<Record<TraxRecord>>) -> Self {
            let record_map = records
                .into_iter()
                .map(|r| (r.id, r))
                .collect::<HashMap<RecordId, Record<TraxRecord>>>();
            Self {
                records: Arc::new(RwLock::new(record_map)),
                put_records: Arc::new(RwLock::new(vec![])),
                updated_records: Arc::new(RwLock::new(vec![])),
                deleted_records: Arc::new(RwLock::new(vec![])),
            }
        }
    }

    #[async_trait]
    impl RecordProvider for MockProvider {
        async fn records(
            &self,
            start: NaiveDate,
            end: NaiveDate,
        ) -> Result<Vec<Record<TraxRecord>>, ReadError> {
            let start = emseries::Timestamp::Date(start);
            let end = emseries::Timestamp::Date(end);
            Ok(self
                .records
                .read()
                .unwrap()
                .iter()
                .map(|(_, r)| r)
                .filter(|r| r.timestamp() >= start && r.timestamp() <= end)
                .cloned()
                .collect::<Vec<Record<TraxRecord>>>())
        }

        async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
            let id = RecordId::default();
            let record = Record {
                id: id,
                data: record,
            };
            self.put_records.write().unwrap().push(record.clone());
            self.records.write().unwrap().insert(id, record);
            Ok(id)
        }

        async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
            println!("updated record: {:?}", record);
            self.updated_records.write().unwrap().push(record.clone());
            self.records.write().unwrap().insert(record.id, record);
            Ok(())
        }

        async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> {
            self.deleted_records.write().unwrap().push(id);
            let _ = self.records.write().unwrap().remove(&id);
            Ok(())
        }
    }

    async fn create_empty_view_model() -> (DayDetailViewModel, MockProvider) {
        let provider = MockProvider::new(vec![]);

        let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
        let model = DayDetailViewModel::new(oct_13, provider.clone())
            .await
            .unwrap();
        (model, provider)
    }

    async fn create_view_model() -> (DayDetailViewModel, MockProvider) {
        let oct_12 = chrono::NaiveDate::from_ymd_opt(2023, 10, 12).unwrap();
        let oct_13 = chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap();
        let oct_13_am: DateTime<FixedOffset> = oct_13
            .clone()
            .and_hms_opt(3, 28, 0)
            .unwrap()
            .and_utc()
            .with_timezone(&FixedOffset::east_opt(5 * 3600).unwrap());
        let provider = MockProvider::new(vec![
            Record {
                id: RecordId::default(),
                data: TraxRecord::Weight(ft_core::Weight {
                    date: oct_12,
                    weight: 93. * si::KG,
                }),
            },
            Record {
                id: RecordId::default(),
                data: TraxRecord::Weight(ft_core::Weight {
                    date: oct_13,
                    weight: 95. * si::KG,
                }),
            },
            Record {
                id: RecordId::default(),
                data: TraxRecord::Steps(ft_core::Steps {
                    date: oct_13,
                    count: 2500,
                }),
            },
            Record {
                id: RecordId::default(),
                data: TraxRecord::TimeDistance(ft_core::TimeDistance {
                    datetime: oct_13_am.clone(),
                    activity: TimeDistanceActivity::Biking,
                    distance: Some(15000. * si::M),
                    duration: Some(3600. * si::S),
                    comments: Some("somecomments present".to_owned()),
                }),
            },
        ]);
        let model = DayDetailViewModel::new(oct_13, provider.clone())
            .await
            .unwrap();
        (model, provider)
    }

    #[tokio::test]
    async fn it_honors_only_the_first_weight_and_step_record() {
        let (view_model, _provider) = create_view_model().await;
        assert_eq!(view_model.weight(), Some(95. * si::KG));
        assert_eq!(view_model.steps(), Some(2500));
    }

    #[tokio::test]
    async fn it_can_create_a_weight_and_stepcount() {
        let (view_model, provider) = create_empty_view_model().await;
        assert_eq!(view_model.weight(), None);
        assert_eq!(view_model.steps(), None);

        view_model.set_weight(95. * si::KG);
        view_model.set_steps(250);

        assert_eq!(view_model.weight(), Some(95. * si::KG));
        assert_eq!(view_model.steps(), Some(250));

        view_model.set_weight(93. * si::KG);
        view_model.set_steps(255);

        assert_eq!(view_model.weight(), Some(93. * si::KG));
        assert_eq!(view_model.steps(), Some(255));

        view_model.async_save().await;

        println!("provider: {:?}", provider);
        assert_eq!(provider.put_records.read().unwrap().len(), 2);
        assert_eq!(provider.updated_records.read().unwrap().len(), 0);
        assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
    }

    #[tokio::test]
    async fn it_can_construct_new_records() {
        let (view_model, provider) = create_empty_view_model().await;
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceActivity::Biking),
            (0. * si::M, 0. * si::S)
        );

        let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking);
        record.data.duration = Some(60. * si::S);
        view_model.async_save().await;

        assert_eq!(provider.put_records.read().unwrap().len(), 1);
        assert_eq!(provider.updated_records.read().unwrap().len(), 0);
        assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
    }

    #[tokio::test]
    async fn it_can_update_a_new_record_before_saving() {
        let (view_model, provider) = create_empty_view_model().await;
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceActivity::Biking),
            (0. * si::M, 0. * si::S)
        );

        let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking);
        record.data.duration = Some(60. * si::S);
        let record = record.map(TraxRecord::TimeDistance);
        view_model.update_record(record.clone());
        assert_eq!(view_model.get_record(&record.id), Some(record));
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceActivity::Biking),
            (0. * si::M, 60. * si::S)
        );
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceActivity::Running),
            (0. * si::M, 0. * si::S)
        );
        view_model.async_save().await;

        assert_eq!(provider.put_records.read().unwrap().len(), 1);
        assert_eq!(provider.updated_records.read().unwrap().len(), 0);
        assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
    }

    #[tokio::test]
    async fn it_can_update_an_existing_record() {
        let (view_model, provider) = create_view_model().await;
        let mut workout = view_model.time_distance_records().first().cloned().unwrap();

        workout.data.duration = Some(1800. * si::S);
        view_model.update_record(workout.map(TraxRecord::TimeDistance));

        assert_eq!(
            view_model.time_distance_summary(TimeDistanceActivity::Biking),
            (15000. * si::M, 1800. * si::S)
        );

        view_model.async_save().await;

        assert_eq!(provider.put_records.read().unwrap().len(), 0);
        assert_eq!(provider.updated_records.read().unwrap().len(), 1);
        assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
    }

    #[tokio::test]
    async fn it_can_remove_a_new_record() {
        let (view_model, provider) = create_empty_view_model().await;
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceActivity::Biking),
            (0. * si::M, 0. * si::S)
        );

        let record = view_model.new_time_distance(TimeDistanceActivity::Biking);
        view_model.remove_record(record.id);
        view_model.save();

        assert_eq!(provider.put_records.read().unwrap().len(), 0);
        assert_eq!(provider.updated_records.read().unwrap().len(), 0);
        assert_eq!(provider.deleted_records.read().unwrap().len(), 0);
    }

    #[tokio::test]
    async fn it_can_delete_an_existing_record() {
        let (view_model, provider) = create_view_model().await;
        let workout = view_model.time_distance_records().first().cloned().unwrap();

        view_model.remove_record(workout.id);
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceActivity::Biking),
            (0. * si::M, 0. * si::S)
        );
        view_model.async_save().await;

        assert_eq!(provider.put_records.read().unwrap().len(), 0);
        assert_eq!(provider.updated_records.read().unwrap().len(), 0);
        assert_eq!(provider.deleted_records.read().unwrap().len(), 1);
    }
}