/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2023 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::path::{Path, PathBuf};

use darkfi_serial::{SerialDecodable, SerialEncodable};
use log::debug;
use serde::{Deserialize, Serialize};

use darkfi::{
    event_graph::gen_id,
    util::{
        file::{load_json_file, save_json_file},
        time::Timestamp,
    },
};

use crate::{
    error::{TaudError, TaudResult},
    month_tasks::MonthTasks,
    util::find_free_id,
};

#[derive(Clone, Debug, Serialize, Deserialize, SerialEncodable, SerialDecodable, PartialEq, Eq)]
pub struct TaskEvent {
    pub action: String,
    pub author: String,
    pub content: String,
    pub timestamp: Timestamp,
}

impl TaskEvent {
    pub fn new(action: String, author: String, content: String) -> Self {
        Self { action, author, content, timestamp: Timestamp::current_time() }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, SerialDecodable, SerialEncodable, PartialEq, Eq)]
pub struct Comment {
    content: String,
    author: String,
    timestamp: Timestamp,
}

impl Comment {
    pub fn new(content: &str, author: &str) -> Self {
        Self {
            content: content.into(),
            author: author.into(),
            timestamp: Timestamp::current_time(),
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SerialEncodable, SerialDecodable)]
pub struct TaskEvents(pub Vec<TaskEvent>);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SerialEncodable, SerialDecodable)]
pub struct TaskComments(Vec<Comment>);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SerialEncodable, SerialDecodable)]
pub struct TaskProjects(Vec<String>);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SerialEncodable, SerialDecodable)]
pub struct TaskAssigns(Vec<String>);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, SerialEncodable, SerialDecodable)]
pub struct TaskTags(Vec<String>);

#[derive(Clone, Debug, Serialize, Deserialize, SerialEncodable, SerialDecodable, PartialEq)]
pub struct TaskInfo {
    pub(crate) ref_id: String,
    pub(crate) workspace: String,
    pub(crate) id: u32,
    pub(crate) title: String,
    tags: TaskTags,
    desc: String,
    pub(crate) owner: String,
    assign: TaskAssigns,
    project: TaskProjects,
    due: Option<Timestamp>,
    rank: Option<f32>,
    created_at: Timestamp,
    state: String,
    pub(crate) events: TaskEvents,
    comments: TaskComments,
}

impl TaskInfo {
    pub fn new(
        workspace: String,
        title: &str,
        desc: &str,
        owner: &str,
        due: Option<Timestamp>,
        rank: Option<f32>,
        dataset_path: &Path,
    ) -> TaudResult<Self> {
        // generate ref_id
        let ref_id = gen_id(30);

        let created_at = Timestamp::current_time();

        let task_ids: Vec<u32> =
            MonthTasks::load_current_tasks(dataset_path, workspace.clone(), false)?
                .into_iter()
                .map(|t| t.id)
                .collect();

        let id: u32 = find_free_id(&task_ids);

        if let Some(d) = &due {
            if *d < Timestamp::current_time() {
                return Err(TaudError::InvalidDueTime)
            }
        }

        Ok(Self {
            ref_id,
            workspace,
            id,
            title: title.into(),
            desc: desc.into(),
            owner: owner.into(),
            tags: TaskTags(vec![]),
            assign: TaskAssigns(vec![]),
            project: TaskProjects(vec![]),
            due,
            rank,
            created_at,
            state: "open".into(),
            comments: TaskComments(vec![]),
            events: TaskEvents(vec![]),
        })
    }

    pub fn load(ref_id: &str, dataset_path: &Path) -> TaudResult<Self> {
        debug!(target: "tau", "TaskInfo::load()");
        let task = load_json_file::<Self>(&Self::get_path(ref_id, dataset_path))?;
        Ok(task)
    }

    pub fn save(&self, dataset_path: &Path) -> TaudResult<()> {
        debug!(target: "tau", "TaskInfo::save()");
        save_json_file::<Self>(&Self::get_path(&self.ref_id, dataset_path), self)
            .map_err(TaudError::Darkfi)?;

        if self.get_state() == "stop" {
            self.deactivate(dataset_path)?;
        } else {
            self.activate(dataset_path)?;
        }

        Ok(())
    }

    pub fn activate(&self, path: &Path) -> TaudResult<()> {
        debug!(target: "tau", "TaskInfo::activate()");
        let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
        mt.add(&self.ref_id);
        mt.save(path)
    }

    pub fn deactivate(&self, path: &Path) -> TaudResult<()> {
        debug!(target: "tau", "TaskInfo::deactivate()");
        let mut mt = MonthTasks::load_or_create(Some(&self.created_at), path)?;
        mt.remove(&self.ref_id);
        mt.save(path)
    }

    pub fn get_state(&self) -> String {
        debug!(target: "tau", "TaskInfo::get_state()");
        self.state.clone()
    }

    pub fn get_path(ref_id: &str, dataset_path: &Path) -> PathBuf {
        debug!(target: "tau", "TaskInfo::get_path()");
        dataset_path.join("task").join(ref_id)
    }

    pub fn get_id(&self) -> u32 {
        debug!(target: "tau", "TaskInfo::get_id()");
        self.id
    }

    pub fn set_title(&mut self, title: &str) {
        debug!(target: "tau", "TaskInfo::set_title()");
        self.title = title.into();
    }

    pub fn set_desc(&mut self, desc: &str) {
        debug!(target: "tau", "TaskInfo::set_desc()");
        self.desc = desc.into();
    }

    pub fn set_tags(&mut self, tags: &[String]) {
        debug!(target: "tau", "TaskInfo::set_tags()");
        for tag in tags.iter() {
            if tag.starts_with('+') && !self.tags.0.contains(tag) {
                self.tags.0.push(tag.to_string());
            }
            if tag.starts_with('-') {
                let t = tag.replace('-', "+");
                self.tags.0.retain(|tag| tag != &t);
            }
        }
    }

    pub fn set_assign(&mut self, assigns: &[String]) {
        debug!(target: "tau", "TaskInfo::set_assign()");
        self.assign = TaskAssigns(assigns.to_owned());
    }

    pub fn set_project(&mut self, projects: &[String]) {
        debug!(target: "tau", "TaskInfo::set_project()");
        self.project = TaskProjects(projects.to_owned());
    }

    pub fn set_comment(&mut self, c: Comment) {
        debug!(target: "tau", "TaskInfo::set_comment()");
        self.comments.0.push(c);
    }

    pub fn set_rank(&mut self, r: Option<f32>) {
        debug!(target: "tau", "TaskInfo::set_rank()");
        self.rank = r;
    }

    pub fn set_due(&mut self, d: Option<Timestamp>) {
        debug!(target: "tau", "TaskInfo::set_due()");
        self.due = d;
    }

    pub fn set_state(&mut self, state: &str) {
        debug!(target: "tau", "TaskInfo::set_state()");
        if self.get_state() == state {
            return
        }
        self.state = state.to_string();
    }
}
