use directories::ProjectDirs;
use secrecy::{ExposeSecret, SecretString};
use std::fs;
use std::io;
use std::path::PathBuf;

use crate::error::{CliError, Result};

const TOKEN_FILE_PERMISSIONS: u32 = 0o600;

pub struct TokenStore {
    service_name: &'static str,
}

impl TokenStore {
    pub fn new(service_name: &'static str) -> Self {
        Self { service_name }
    }

    pub fn micropub() -> Self {
        Self::new("micropub")
    }

    pub fn indieauth() -> Self {
        Self::new("indieauth")
    }

    fn token_path(&self) -> Option<PathBuf> {
        ProjectDirs::from("org", "indieweb", "indieweb").map(|dirs| {
            dirs.data_local_dir()
                .join(format!("{}-token", self.service_name))
        })
    }

    pub fn load(&self) -> Result<Option<SecretString>> {
        let path = self.token_path().ok_or_else(|| {
            CliError::Config(Box::new(io::Error::new(
                io::ErrorKind::NotFound,
                "Could not determine token storage path",
            )))
        })?;

        if !path.exists() {
            return Ok(None);
        }

        let contents = fs::read_to_string(&path)?;
        let token = contents.trim().to_string();

        if token.is_empty() {
            Ok(None)
        } else {
            Ok(Some(SecretString::new(token.into_boxed_str())))
        }
    }

    pub fn save(&self, token: &SecretString) -> Result<()> {
        let path = self.token_path().ok_or_else(|| {
            CliError::Config(Box::new(io::Error::new(
                io::ErrorKind::NotFound,
                "Could not determine token storage path",
            )))
        })?;

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

        fs::write(&path, token.expose_secret())?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            fs::set_permissions(&path, fs::Permissions::from_mode(TOKEN_FILE_PERMISSIONS))?;
        }

        Ok(())
    }

    pub fn delete(&self) -> Result<()> {
        if let Some(path) = self.token_path() {
            if path.exists() {
                fs::remove_file(&path)?;
            }
        }
        Ok(())
    }

    pub fn exists(&self) -> bool {
        self.token_path().map(|p| p.exists()).unwrap_or(false)
    }

    pub fn resolve_token(
        &self,
        cli_token: Option<&String>,
        env_token: Option<&String>,
    ) -> Result<Option<SecretString>> {
        if let Some(token) = cli_token {
            return Ok(Some(SecretString::new(token.clone().into_boxed_str())));
        }

        if let Some(token) = env_token {
            return Ok(Some(SecretString::new(token.clone().into_boxed_str())));
        }

        self.load()
    }
}
