/*
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, WriteError};
use chrono::NaiveDate;
use dimensioned::si;
use emseries::{Record, RecordId, Recordable};
use ft_core::{RecordType, TimeDistance, TimeDistanceWorkoutType, TraxRecord};
use std::{
    collections::HashMap,
    ops::Deref,
    sync::{Arc, RwLock},
};

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

impl<T: Clone + emseries::Recordable> RecordState<T> {
    #[allow(unused)]
    fn id(&self) -> Option<&RecordId> {
        match self {
            RecordState::Original(ref r) => Some(&r.id),
            RecordState::New(ref r) => None,
            RecordState::Updated(ref r) => Some(&r.id),
            RecordState::Deleted(ref r) => Some(&r.id),
        }
    }

    fn with_value(self, value: T) -> RecordState<T> {
        match self {
            RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }),
            RecordState::New(_) => RecordState::New(value),
            RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }),
            RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }),
        }
    }

    #[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,
            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) => r,
            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 (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
            provider
                .records(date, date)
                .await?
                .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());

        if weight_records.len() > 1 {
            eprintln!("warning: multiple weight records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d"));
        }
        if step_records.len() > 1 {
            eprintln!("warning: multiple step records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d"));
        }

        Ok(Self {
            provider: Arc::new(provider),
            date,
            weight: Arc::new(RwLock::new(
                weight_records
                    .first()
                    .and_then(|r| match r.data {
                        TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())),
                        _ => None,
                    })
                    .map(|(id, w)| RecordState::Original(Record { id, data: w })),
            )),
            steps: Arc::new(RwLock::new(
                step_records
                    .first()
                    .and_then(|r| match r.data {
                        TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())),
                        _ => None,
                    })
                    .map(|(id, w)| RecordState::Original(Record { id, data: w })),
            )),

            records: Arc::new(RwLock::new(
                records
                    .into_iter()
                    .map(|r| (r.id.clone(), RecordState::Original(r)))
                    .collect::<HashMap<RecordId, RecordState<TraxRecord>>>(),
            )),
        })
    }

    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(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(ft_core::Steps {
                date: self.date,
                count: new_count,
            }),
        };
        *record = Some(new_record);
    }

    pub fn new_time_distance(&self, type_: TimeDistanceWorkoutType) -> Record<TimeDistance> {
        let id = RecordId::default();
        let workout = TimeDistance {
            datetime: chrono::Local::now().into(),
            distance: None,
            duration: None,
            comments: None,
        };
        let tr = TraxRecord::from_time_distance(type_, workout.clone());
        self.records
            .write()
            .unwrap()
            .insert(id.clone(), RecordState::New(tr));
        println!(
            "records after new_time_distance: {:?}",
            self.records.read().unwrap()
        );
        Record { id, data: workout }
    }

    pub fn update_time_distance(&self, workout: Record<TimeDistance>) {
        let id = workout.id.clone();
        let data = workout.data.clone();

        self.records
            .write()
            .unwrap()
            .entry(id)
            .and_modify(|r| match **r {
                TraxRecord::BikeRide(ref mut v) => *v = data,
                TraxRecord::Row(ref mut v) => *v = data,
                TraxRecord::Run(ref mut v) => *v = data,
                TraxRecord::Swim(ref mut v) => *v = data,
                TraxRecord::Walk(ref mut v) => *v = data,
                _ => {}
            });
    }

    pub fn time_distance_records(&self, type_: RecordType) -> Vec<Record<TimeDistance>> {
        unimplemented!("time_distance_records")
    }

    pub fn time_distance_summary(
        &self,
        type_: TimeDistanceWorkoutType,
    ) -> (si::Meter<f64>, si::Second<f64>) {
        self.records
            .read()
            .unwrap()
            .iter()
            .filter(|(_, workout_state)| workout_state.is_time_distance_type(type_))
            .filter_map(|(id, record_state)| match **record_state {
                TraxRecord::BikeRide(ref workout)
                | TraxRecord::Row(ref workout)
                | TraxRecord::Run(ref workout)
                | TraxRecord::Swim(ref workout)
                | TraxRecord::Walk(ref workout) => Some(workout),
                _ => None,
            })
            .fold((0. * si::M, 0. * si::S), |(distance, duration), workout| {
                println!("folding workout: {:?}", workout);
                match (workout.distance, workout.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),
                }
            })
    }

    fn get_record(&self, id: &RecordId) -> Option<Record<TraxRecord>> {
        let record_set = self.records.read().unwrap();
        match record_set.get(&id) {
            Some(record) => Some(Record {
                id: id.clone(),
                data: (**record).clone(),
            }),
            None => None,
        }
    }

    pub fn remove_record(&self, id: RecordId) {
        unimplemented!("remove_record")
    }

    pub fn save(&self) {
        glib::spawn_future({
            let s = self.clone();
            async move { s.async_save().await }
        });
    }

    pub async fn async_save(&self) {
        println!("async_save");
        let weight_record = self.weight.read().unwrap().clone();
        match weight_record {
            Some(RecordState::New(data)) => {
                let _ = self.provider.put_record(TraxRecord::Weight(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)).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).await;
                }
                RecordState::Original(_) => {}
                RecordState::Updated(r) => {
                    let _ = self.provider.update_record(r.clone()).await;
                }
                RecordState::Deleted(_) => unimplemented!(),
            }
        }
    }

    pub fn revert(&self) {
        unimplemented!();
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use async_trait::async_trait;
    use chrono::{DateTime, FixedOffset, TimeZone};
    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.clone(), 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.clone(),
                data: record,
            };
            self.put_records.write().unwrap().push(record.clone());
            self.records.write().unwrap().insert(id.clone(), record);
            Ok(id)
        }

        async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
            Err(WriteError::NoDatabase)
        }

        async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> {
            Err(WriteError::NoDatabase)
        }
    }

    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.clone(),
                    weight: 93. * si::KG,
                }),
            },
            Record {
                id: RecordId::default(),
                data: TraxRecord::Weight(ft_core::Weight {
                    date: oct_13.clone(),
                    weight: 95. * si::KG,
                }),
            },
            Record {
                id: RecordId::default(),
                data: TraxRecord::Steps(ft_core::Steps {
                    date: oct_13.clone(),
                    count: 2500,
                }),
            },
            Record {
                id: RecordId::default(),
                data: TraxRecord::BikeRide(ft_core::TimeDistance {
                    datetime: oct_13_am.clone(),
                    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(TimeDistanceWorkoutType::BikeRide),
            (0. * si::M, 0. * si::S)
        );

        let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide);
        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(TimeDistanceWorkoutType::BikeRide),
            (0. * si::M, 0. * si::S)
        );

        let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide);
        record.data.duration = Some(60. * si::S);
        view_model.update_time_distance(record.clone());
        let record = Record {
            id: record.id,
            data: TraxRecord::BikeRide(record.data),
        };
        assert_eq!(view_model.get_record(&record.id), Some(record));
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide),
            (0. * si::M, 60. * si::S)
        );
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceWorkoutType::Run),
            (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]
    #[ignore]
    async fn it_can_update_an_existing_record() {
        let (view_model, provider) = create_view_model().await;
        let mut workout = view_model
            .time_distance_records(RecordType::BikeRide)
            .first()
            .cloned()
            .unwrap();

        workout.data.duration = Some(1800. * si::S);
        view_model.update_time_distance(workout.clone());

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

        view_model.save();

        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]
    #[ignore]
    async fn it_can_remove_a_new_record() {
        let (view_model, provider) = create_empty_view_model().await;
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide),
            (0. * si::M, 0. * si::S)
        );

        let record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide);
        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]
    #[ignore]
    async fn it_can_delete_an_existing_record() {
        let (view_model, provider) = create_view_model().await;
        let mut workout = view_model
            .time_distance_records(RecordType::BikeRide)
            .first()
            .cloned()
            .unwrap();

        view_model.remove_record(workout.id);
        assert_eq!(
            view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide),
            (0. * si::M, 0. * si::S)
        );
        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(), 1);
    }
}