Skip to content
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Usage {
    /// Number of issues created per repo (keyed by full_name, e.g. "org/repo").
    #[serde(default)]
    pub repos: HashMap<String, u32>,
    /// Number of issues created per project (keyed by project id).
    #[serde(default)]
    pub projects: HashMap<String, u32>,
}

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Cache {
    pub repos: Vec<Repo>,
    pub projects: Vec<Project>,
    #[serde(default)]
    pub usage: Usage,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Repo {
    pub name: String,
    pub full_name: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Project {
    pub id: String,
    pub title: String,
    pub status_field_id: String,
    pub status_options: Vec<StatusOption>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct StatusOption {
    pub id: String,
    pub name: String,
}

impl Cache {
    pub fn dir() -> Result<PathBuf> {
        dirs::cache_dir()
            .map(|d| d.join("pike"))
            .context("Could not determine cache directory")
    }

    pub fn load() -> Result<Self> {
        let path = Self::dir()?.join("cache.json");
        if !path.exists() {
            return Ok(Self::default());
        }
        let content = std::fs::read_to_string(&path).context("Failed to read cache file")?;
        serde_json::from_str(&content).context("Failed to parse cache file")
    }

    /// Increments usage counters for the given repo and (optionally) project,
    /// then saves the cache. Call this after a successful issue creation.
    pub fn record_issue(&mut self, repo_full_name: &str, project_id: Option<&str>) -> Result<()> {
        *self
            .usage
            .repos
            .entry(repo_full_name.to_string())
            .or_insert(0) += 1;
        if let Some(pid) = project_id {
            *self.usage.projects.entry(pid.to_string()).or_insert(0) += 1;
        }
        self.save()
    }

    pub fn save(&self) -> Result<()> {
        let dir = Self::dir()?;
        std::fs::create_dir_all(&dir).context("Failed to create cache directory")?;
        let path = dir.join("cache.json");
        let content = serde_json::to_string_pretty(self).context("Failed to serialize cache")?;
        std::fs::write(&path, content).context("Failed to write cache file")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cache_default() {
        let cache = Cache::default();
        assert!(cache.repos.is_empty());
        assert!(cache.projects.is_empty());
    }

    #[test]
    fn cache_roundtrip() {
        let cache = Cache {
            repos: vec![
                Repo {
                    name: "repo1".to_string(),
                    full_name: "org/repo1".to_string(),
                },
                Repo {
                    name: "repo2".to_string(),
                    full_name: "org/repo2".to_string(),
                },
            ],
            projects: vec![Project {
                id: "PVT_123".to_string(),
                title: "My Project".to_string(),
                status_field_id: "PVTFL_456".to_string(),
                status_options: vec![
                    StatusOption {
                        id: "opt1".to_string(),
                        name: "Todo".to_string(),
                    },
                    StatusOption {
                        id: "opt2".to_string(),
                        name: "Done".to_string(),
                    },
                ],
            }],
        };

        let json = serde_json::to_string_pretty(&cache).unwrap();
        let deserialized: Cache = serde_json::from_str(&json).unwrap();

        assert_eq!(deserialized.repos.len(), 2);
        assert_eq!(deserialized.repos[0].full_name, "org/repo1");
        assert_eq!(deserialized.projects.len(), 1);
        assert_eq!(deserialized.projects[0].title, "My Project");
        assert_eq!(deserialized.projects[0].status_options.len(), 2);
        assert_eq!(deserialized.projects[0].status_options[0].name, "Todo");
    }

    #[test]
    fn cache_save_and_load() {
        let dir = std::env::temp_dir().join("pike_test_cache");
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("cache.json");

        let cache = Cache {
            repos: vec![Repo {
                name: "test".to_string(),
                full_name: "org/test".to_string(),
            }],
            projects: vec![],
        };

        let content = serde_json::to_string_pretty(&cache).unwrap();
        std::fs::write(&path, &content).unwrap();

        let loaded: Cache = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
        assert_eq!(loaded.repos.len(), 1);
        assert_eq!(loaded.repos[0].full_name, "org/test");

        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn repo_equality() {
        let repo1 = Repo {
            name: "pike".to_string(),
            full_name: "anomalyco/pike".to_string(),
        };
        let repo2 = Repo {
            name: "pike".to_string(),
            full_name: "anomalyco/pike".to_string(),
        };
        assert_eq!(repo1.name, repo2.name);
        assert_eq!(repo1.full_name, repo2.full_name);
    }

    #[test]
    fn status_option_deserialize() {
        let json = r#"{"id": "abc", "name": "In Progress"}"#;
        let opt: StatusOption = serde_json::from_str(json).unwrap();
        assert_eq!(opt.id, "abc");
        assert_eq!(opt.name, "In Progress");
    }
}