use crate::Result;
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fs,
    path::{Path, PathBuf},
};

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct StateFile {
    #[serde(default)]
    pub profiles: HashMap<String, ProfileState>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ProfileState {
    #[serde(default)]
    pub user_id: Option<String>,
    #[serde(default)]
    pub semesters: HashMap<String, SemesterState>,
    #[serde(default)]
    pub courses: HashMap<String, CourseState>,
    #[serde(default)]
    pub files: HashMap<String, FileState>,
}

#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct SemesterState {
    pub id: String,
    pub title: String,
    pub key: String,
}

#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct CourseState {
    pub id: String,
    pub name: String,
    pub semester_key: String,
    #[serde(default)]
    pub last_sync: Option<String>,
}

#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct FileState {
    pub id: String,
    #[serde(default)]
    pub size: Option<u64>,
    #[serde(default)]
    pub checksum: Option<String>,
    #[serde(default)]
    pub last_downloaded: Option<String>,
    #[serde(default)]
    pub remote_modified: Option<String>,
    #[serde(default)]
    pub path_hint: Option<PathBuf>,
}

impl StateFile {
    pub fn load_or_default(path: &Path) -> Result<Self> {
        match fs::read_to_string(path) {
            Ok(contents) => Ok(toml::from_str(&contents)?),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
            Err(err) => Err(err.into()),
        }
    }

    pub fn save(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let contents = toml::to_string_pretty(self)?;
        fs::write(path, contents)?;
        Ok(())
    }

    pub fn profile_mut(&mut self, profile: &str) -> &mut ProfileState {
        self.profiles
            .entry(profile.to_string())
            .or_insert_with(ProfileState::default)
    }

    pub fn profile(&self, profile: &str) -> Option<&ProfileState> {
        self.profiles.get(profile)
    }
}
