/*
Copyright 2023, 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 async_trait::async_trait;
use chrono::NaiveDate;
use emseries::{time_range, Record, RecordId, Series, Timestamp};
use ft_core::TraxRecord;
use std::{
    path::PathBuf,
    sync::{Arc, RwLock},
};
use thiserror::Error;
use tokio::runtime::Runtime;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("no database loaded")]
    NoDatabase,
    #[error("failed to open the database")]
    FailedToOpenDatabase,
    #[error("unhandled error")]
    Unhandled,
}

#[derive(Debug, Error)]
pub enum ReadError {
    #[error("no database loaded")]
    NoDatabase,
}

#[derive(Debug, Error)]
pub enum WriteError {
    #[error("no database loaded")]
    NoDatabase,
    #[error("unhandled error")]
    Unhandled,
}

#[async_trait]
pub trait RecordProvider: Send + Sync {
    async fn records(
        &self,
        start: NaiveDate,
        end: NaiveDate,
    ) -> Result<Vec<Record<TraxRecord>>, ReadError>;
    async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError>;
    async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError>;
    async fn delete_record(&self, id: RecordId) -> Result<(), WriteError>;
}

/// The real, headless application. This is where all of the logic will reside.
#[derive(Clone)]
pub struct App {
    runtime: Arc<Runtime>,
    database: Arc<RwLock<Option<Series<TraxRecord>>>>,
}

impl App {
    pub fn new(db_path: Option<PathBuf>) -> Self {
        let database = db_path.map(|path| Series::open(path).unwrap());
        let runtime = Arc::new(
            tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .unwrap(),
        );

        Self {
            runtime,
            database: Arc::new(RwLock::new(database)),
        }
    }

    pub async fn open_db(&self, path: PathBuf) -> Result<(), AppError> {
        let db_ref = self.database.clone();
        self.runtime
            .spawn_blocking(move || {
                let db = Series::open(path).map_err(|_| AppError::FailedToOpenDatabase)?;
                *db_ref.write().unwrap() = Some(db);
                Ok(())
            })
            .await
            .unwrap()
    }

    pub fn database_is_open(&self) -> bool {
        self.database.read().unwrap().is_some()
    }
}

#[async_trait]
impl RecordProvider for App {
    async fn records(
        &self,
        start: NaiveDate,
        end: NaiveDate,
    ) -> Result<Vec<Record<TraxRecord>>, ReadError> {
        let db = self.database.clone();
        self.runtime
            .spawn_blocking(move || {
                if let Some(ref db) = *db.read().unwrap() {
                    let records = db
                        .search(time_range(
                            Timestamp::Date(start),
                            true,
                            Timestamp::Date(end),
                            true,
                        ))
                        .cloned()
                        .collect::<Vec<Record<TraxRecord>>>();
                    Ok(records)
                } else {
                    Err(ReadError::NoDatabase)
                }
            })
            .await
            .unwrap()
    }

    async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
        let db = self.database.clone();
        self.runtime
            .spawn_blocking(move || {
                if let Some(ref mut db) = *db.write().unwrap() {
                    let id = db.put(record).unwrap();
                    Ok(id)
                } else {
                    Err(AppError::NoDatabase)
                }
            })
            .await
            .unwrap()
            .map_err(|_| WriteError::Unhandled)
    }

    async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
        let db = self.database.clone();
        self.runtime
            .spawn_blocking(move || {
                if let Some(ref mut db) = *db.write().unwrap() {
                    db.update(record).map_err(|_| AppError::Unhandled)
                } else {
                    Err(AppError::NoDatabase)
                }
            })
            .await
            .unwrap()
            .map_err(|_| WriteError::Unhandled)
    }

    async fn delete_record(&self, _id: RecordId) -> Result<(), WriteError> {
        unimplemented!()
    }
}
