use std::{
    fs,
    str::{from_utf8, FromStr},
};

use chrono::NaiveDate;
use rocksdb::{IteratorMode, Options, DB};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Todo {
    pub name: String,
    pub status: Status,
    pub due_date: String,
    pub note: String,
    pub tags: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum Status {
    ToDo,
    Done,
}

impl ToString for Status {
    fn to_string(&self) -> String {
        match self {
            Status::ToDo => String::from("To Do"),
            Status::Done => String::from("Done"),
        }
    }
}

impl FromStr for Status {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "ToDo" => Ok(Status::ToDo),
            "Done" => Ok(Status::Done),
            _ => Err(format!("{} is not a valid status", s)),
        }
    }
}

pub fn add_todo(
    db: &DB,
    key: &String,
    note_arg: Option<&String>,
    due_date_arg: Option<&String>,
) -> Result<(), &'static str> {
    let res = db.get(key).unwrap();
    if res.is_some() {
        return Err("Todo with this name already exists");
    }

    let mut note = String::from("");
    if note_arg.is_some() {
        note = note_arg.unwrap().to_owned();
    }

    let mut due_date = String::from("");
    if due_date_arg.is_some() {
        if NaiveDate::parse_from_str(due_date_arg.unwrap(), "%d-%m-%Y").is_err() {
            return Err("Invalid date format");
        }

        due_date = due_date_arg.unwrap().to_owned();
    }

    let todo = Todo {
        name: key.to_string(),
        status: Status::ToDo,
        due_date,
        note,
        tags: Vec::new(),
    };
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn get_all_todos(db: &DB, status: Option<&String>, tag: Option<&String>) -> Vec<Todo> {
    let iter = db.iterator(IteratorMode::Start);
    let mut todos: Vec<Todo> = Vec::new();

    for item in iter {
        let (_, todo) = item.unwrap();
        let todo = from_utf8(&todo).unwrap();

        todos.push(serde_json::from_str(todo).unwrap());
    }

    if status.is_some() {
        let status = Status::from_str(status.unwrap()).unwrap();
        if status == Status::Done {
            todos.retain(|t| t.status == Status::Done)
        } else if status == Status::ToDo {
            todos.retain(|t| t.status == Status::ToDo)
        }
    }

    if tag.is_some() {
        let tag = tag.unwrap();
        todos.retain(|t| t.tags.contains(&tag.to_string()));
    }

    todos
}

pub fn complete_todo(db: &DB, key: &String) -> Result<(), &'static str> {
    let res = db.get(key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }
    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    todo.status = Status::Done;
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn uncomplete_todo(db: &DB, key: &String) -> Result<(), &'static str> {
    let res = db.get(key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }
    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    todo.status = Status::ToDo;
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn add_todo_note(db: &DB, key: &String, note: &String) -> Result<(), &'static str> {
    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }
    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    if !todo.note.is_empty() {
        return Err("This todo already has a note");
    }
    todo.note = note.to_string();
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn edit_todo_note(db: &DB, key: &String, new_note: &String) -> Result<(), &'static str> {
    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }
    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    todo.note = new_note.to_string();
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn remove_todo_note(db: &DB, key: &String) -> Result<(), &'static str> {
    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }
    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    todo.note = String::from("");
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn add_todo_tag(db: &DB, key: &String, tag: &String) -> Result<(), &'static str> {
    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }
    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();

    if todo.tags.contains(tag) {
        return Err("This tag is has already been added to this todo");
    }

    todo.tags.push(tag.to_string());
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn remove_todo_tag(db: &DB, key: &String, tag: &String) -> Result<(), &'static str> {
    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }
    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    if !todo.tags.contains(tag) {
        return Err("This tag does not exist for this todo");
    }

    todo.tags.retain(|t| t != tag);
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn add_due_date(db: &DB, key: &String, date: &String) -> Result<(), &'static str> {
    let date = NaiveDate::parse_from_str(date, "%d-%m-%Y");
    if date.is_err() {
        return Err("Invalid date format");
    }

    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }

    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    todo.due_date = date.unwrap().format("%d-%m-%Y").to_string();
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn change_due_date(db: &DB, key: &String, new_date: &String) -> Result<(), &'static str> {
    let date = NaiveDate::parse_from_str(new_date, "%d-%m-%Y");
    if date.is_err() {
        return Err("Invalid date format");
    }

    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }

    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    todo.due_date = date.unwrap().format("%d-%m-%Y").to_string();
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn remove_due_date(db: &DB, key: &String) -> Result<(), &'static str> {
    let res = db.get(&key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }

    let val = String::from_utf8(res.unwrap()).unwrap();

    let mut todo: Todo = serde_json::from_str(&val).unwrap();
    todo.due_date = String::from("");
    let serialized = serde_json::to_string(&todo).unwrap();
    db.put(key, serialized).unwrap();

    Ok(())
}

pub fn delete_todo(db: &DB, key: &String) -> Result<(), &'static str> {
    let res = db.get(key).unwrap();
    if res.is_none() {
        return Err("Todo with this name does not exist");
    }

    db.delete(&key).unwrap();

    Ok(())
}

pub fn drop_db(path: &str) -> std::io::Result<()> {
    let _ = DB::destroy(&Options::default(), path);

    fs::remove_dir_all(path)?;

    Ok(())
}
