trackWork 0.14.2

A terminal-based time tracking application for managing work sessions
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

use crate::integrations::IntegrationKind;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    #[serde(default)]
    pub integration: IntegrationKind,
    #[serde(default = "default_open_command")]
    pub open_command: String,
    #[serde(default = "default_open_worklog_command")]
    pub open_worklog_command: String,
    #[serde(default = "default_colors")]
    pub colors: [String; 6],
    #[serde(default = "default_date_format")]
    pub date_format: String,
    #[serde(default = "default_legacy_time_format")]
    pub legacy_time_format: bool,
    #[serde(default)]
    pub hide_eye_candy: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub jira_url_setting: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub jira_email: Option<String>,
    #[serde(default)]
    pub triggers: TriggersConfig,
    // Legacy fields for backward compatibility
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jira_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub browser_command: Option<String>,
}

/// Webhook callbacks fired on lifecycle events. Each event POSTs its templated
/// JSON body to the configured URL when enabled.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TriggersConfig {
    #[serde(default)]
    pub day_start: TriggerConfig,
    #[serde(default)]
    pub ooo_start: TriggerConfig,
    #[serde(default)]
    pub ooo_end: TriggerConfig,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TriggerConfig {
    #[serde(default)]
    pub enabled: bool,
    #[serde(default)]
    pub url: String,
    #[serde(default)]
    pub body: String,
}

fn default_colors() -> [String; 6] {
    [
        "Blue".to_string(),
        "Cyan".to_string(),
        "Green".to_string(),
        "Yellow".to_string(),
        "Magenta".to_string(),
        "Red".to_string(),
    ]
}

fn default_date_format() -> String {
    "%d.%m.-%y".to_string()
}

fn default_legacy_time_format() -> bool {
    false
}

fn default_open_command() -> String {
    // Default command template for logging work via JIRA REST API v2
    return "curl -X POST https://jira.example.com/rest/api/2/issue/[[issue_key]]/worklog -H \"Content-Type: application/json\" -d '{\"timeSpent\":\"[[task_duration]]\",\"started\":\"[[entry_started]]\",\"comment\":\"Work logged from TimeTrack\"}'".to_string();
}

fn default_open_worklog_command() -> String {
    // Default command template for opening issue (same as open_command)
    #[cfg(target_os = "linux")]
    return "xdg-open https://jira.example.com/browse/[[issue_key]]".to_string();

    #[cfg(target_os = "macos")]
    return "open https://jira.example.com/browse/[[issue_key]]".to_string();

    #[cfg(target_os = "windows")]
    return "start https://jira.example.com/browse/[[issue_key]]".to_string();

    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    return "xdg-open https://jira.example.com/browse/[[issue_key]]".to_string();
}

impl Config {
    pub fn color_names() -> [&'static str; 6] {
        ["Color 1", "Color 2", "Color 3", "Color 4", "Color 5", "Color 6"]
    }

    pub fn available_colors() -> [&'static str; 16] {
        [
            "Blue", "Cyan", "Green", "Yellow", "Magenta", "Red",
            "LightBlue", "LightCyan", "LightGreen", "LightYellow",
            "LightMagenta", "LightRed", "DarkGray", "Gray", "White", "Black"
        ]
    }

    pub fn available_variables() -> [&'static str; 5] {
        ["[[issue_key]]", "[[entry_started]]", "[[entry_ended]]", "[[task_duration]]", "[[description]]"]
    }

    pub fn available_trigger_variables() -> [&'static str; 5] {
        ["[[event]]", "[[date]]", "[[time]]", "[[datetime]]", "[[description]]"]
    }

    /// Substitute trigger variables in a webhook body template.
    pub fn substitute_trigger_variables(
        body: &str,
        event: &str,
        date: &str,
        time: &str,
        datetime: &str,
        description: &str,
    ) -> String {
        body.replace("[[event]]", event)
            .replace("[[date]]", date)
            .replace("[[time]]", time)
            .replace("[[datetime]]", datetime)
            .replace("[[description]]", description)
    }

    /// Substitute variables in the open command
    pub fn substitute_variables(
        &self,
        issue_key: &str,
        entry_started: &str,
        entry_ended: &str,
        task_duration: &str,
        description: &str,
    ) -> String {
        self.open_command
            .replace("[[issue_key]]", issue_key)
            .replace("[[entry_started]]", entry_started)
            .replace("[[entry_ended]]", entry_ended)
            .replace("[[task_duration]]", task_duration)
            .replace("[[description]]", description)
    }
}

impl Default for Config {
    fn default() -> Self {
        Config {
            integration: IntegrationKind::CustomCommands,
            open_command: default_open_command(),
            open_worklog_command: default_open_worklog_command(),
            colors: [
                "Blue".to_string(),
                "Cyan".to_string(),
                "Green".to_string(),
                "Yellow".to_string(),
                "Magenta".to_string(),
                "Red".to_string(),
            ],
            date_format: "%d.%m.-%y".to_string(),
            legacy_time_format: false,
            hide_eye_candy: false,
            jira_url_setting: None,
            jira_email: None,
            triggers: TriggersConfig::default(),
            jira_url: None,
            browser_command: None,
        }
    }
}

impl Config {
    pub fn load() -> Result<Self> {
        let config_path = Self::config_path()?;

        if config_path.exists() {
            let contents = fs::read_to_string(&config_path)?;
            let mut config: Config = toml::from_str(&contents)?;

            // Migrate old config format
            if let (Some(jira_url), Some(browser_command)) = (&config.jira_url, &config.browser_command) {
                // Convert old format to new open_command
                config.open_command = format!("{} {}/[[issue_key]]", browser_command, jira_url);
                config.jira_url = None;
                config.browser_command = None;
                config.save()?;
            }

            Ok(config)
        } else {
            let config = Config::default();
            config.save()?;
            Ok(config)
        }
    }

    pub fn save(&self) -> Result<()> {
        let config_path = Self::config_path()?;

        if let Some(parent) = config_path.parent() {
            fs::create_dir_all(parent)?;
        }

        let contents = toml::to_string_pretty(self)?;
        fs::write(&config_path, contents)?;
        Ok(())
    }

    fn config_path() -> Result<PathBuf> {
        let config_dir = dirs::home_dir()
            .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
        Ok(config_dir.join(".timetrack.toml"))
    }
}