//! Optional JSON store under the OS "local data" dir (e.g. `%LOCALAPPDATA%\\dexter-sentinel\\`
//! on Windows) so connection flags need not be repeated every run.

use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StoredConnection {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub redis_url: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub athena_url: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub athena_key: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub athena_client: Option<String>,
}

fn trim_opt(s: Option<String>) -> Option<String> {
    s.and_then(|v| {
        let t = v.trim();
        (!t.is_empty()).then(|| t.to_string())
    })
}

impl StoredConnection {
    /// Drop empty / whitespace-only strings (after trim).
    pub fn normalized(self) -> Self {
        Self {
            redis_url: trim_opt(self.redis_url),
            athena_url: trim_opt(self.athena_url),
            athena_key: trim_opt(self.athena_key),
            athena_client: trim_opt(self.athena_client),
        }
    }

    pub fn merge_with_cli_and_file(
        cli: &StoredConnection,
        file: StoredConnection,
    ) -> StoredConnection {
        StoredConnection {
            redis_url: trim_opt(cli.redis_url.clone()).or(trim_opt(file.redis_url)),
            athena_url: trim_opt(cli.athena_url.clone()).or(trim_opt(file.athena_url)),
            athena_key: trim_opt(cli.athena_key.clone()).or(trim_opt(file.athena_key)),
            athena_client: trim_opt(cli.athena_client.clone()).or(trim_opt(file.athena_client)),
        }
    }

    pub fn any_set(&self) -> bool {
        self.redis_url.is_some()
            || self.athena_url.is_some()
            || self.athena_key.is_some()
            || self.athena_client.is_some()
    }
}

pub fn credentials_path() -> Option<PathBuf> {
    dirs::data_local_dir().map(|d| d.join("dexter-sentinel").join("credentials.json"))
}

pub fn load() -> StoredConnection {
    let Some(path) = credentials_path() else {
        return StoredConnection::default();
    };
    let Ok(bytes) = fs::read(&path) else {
        return StoredConnection::default();
    };
    serde_json::from_slice::<StoredConnection>(&bytes)
        .unwrap_or_default()
        .normalized()
}

pub fn save(conn: &StoredConnection) -> io::Result<()> {
    if !conn.any_set() {
        return Ok(());
    }
    let Some(path) = credentials_path() else {
        return Ok(());
    };
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let json = serde_json::to_vec_pretty(conn).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    #[cfg(unix)]
    {
        use std::fs::OpenOptions;
        use std::os::unix::fs::OpenOptionsExt;
        let mut f = OpenOptions::new()
            .create(true)
            .truncate(true)
            .write(true)
            .mode(0o600)
            .open(&path)?;
        f.write_all(&json)?;
        f.sync_all()?;
        return Ok(());
    }

    #[cfg(not(unix))]
    {
        let mut f = fs::File::create(&path)?;
        f.write_all(&json)?;
        f.sync_all()?;
        Ok(())
    }
}

/// Human-readable path for `--help` / logs.
pub fn credentials_path_display() -> String {
    credentials_path()
        .map(|p| p.display().to_string())
        .unwrap_or_else(|| "(no local data directory)".to_string())
}
