use std::path::{Path, PathBuf};

use serde::Deserialize;
use thiserror::Error;

const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com";
const DEFAULT_DEEPSEEK_MODEL: &str = "deepseek-chat";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeepSeekSettings {
    pub api_key: String,
    pub base_url: String,
    pub model: String,
}

impl DeepSeekSettings {
    pub fn from_env() -> Result<Self, ConfigError> {
        Self::maybe_from_env()?.ok_or(ConfigError::MissingEnv("DEEPSEEK_API_KEY"))
    }

    pub fn load(config_path: Option<&Path>) -> Result<Option<Self>, ConfigError> {
        if let Some(path) = config_path {
            if path.exists() {
                return Self::from_config_path(path).map(Some);
            }
        }

        Self::maybe_from_env()
    }

    fn maybe_from_env() -> Result<Option<Self>, ConfigError> {
        let api_key = match std::env::var("DEEPSEEK_API_KEY") {
            Ok(value) => value,
            Err(std::env::VarError::NotPresent) => return Ok(None),
            Err(std::env::VarError::NotUnicode(_)) => {
                return Err(ConfigError::InvalidEnv("DEEPSEEK_API_KEY"))
            }
        };
        let base_url = std::env::var("DEEPSEEK_BASE_URL")
            .unwrap_or_else(|_| DEFAULT_DEEPSEEK_BASE_URL.to_string());
        let model =
            std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| DEFAULT_DEEPSEEK_MODEL.to_string());

        Ok(Some(Self::new(api_key, base_url, model)?))
    }

    fn from_config_path(path: &Path) -> Result<Self, ConfigError> {
        let raw = std::fs::read_to_string(path)
            .map_err(|err| ConfigError::ConfigRead(path.to_path_buf(), err.to_string()))?;
        let config: RawDeepSeekSettings = serde_json::from_str(&raw)
            .map_err(|err| ConfigError::ConfigParse(path.to_path_buf(), err.to_string()))?;

        Self::new(config.api_key, config.base_url, config.model).map_err(|err| err.with_path(path))
    }

    fn new(api_key: String, base_url: String, model: String) -> Result<Self, ConfigError> {
        let api_key = api_key.trim().to_string();
        let base_url = if base_url.trim().is_empty() {
            DEFAULT_DEEPSEEK_BASE_URL.to_string()
        } else {
            base_url.trim().to_string()
        };
        let model = if model.trim().is_empty() {
            DEFAULT_DEEPSEEK_MODEL.to_string()
        } else {
            model.trim().to_string()
        };

        if api_key.is_empty() {
            return Err(ConfigError::EmptyValue("DEEPSEEK_API_KEY"));
        }
        if base_url.is_empty() {
            return Err(ConfigError::EmptyValue("DEEPSEEK_BASE_URL"));
        }
        if model.is_empty() {
            return Err(ConfigError::EmptyValue("DEEPSEEK_MODEL"));
        }

        Ok(Self {
            api_key,
            base_url,
            model,
        })
    }
}

#[derive(Debug, Deserialize)]
struct RawDeepSeekSettings {
    #[serde(rename = "apiKey", default)]
    api_key: String,
    #[serde(rename = "baseUrl", default)]
    base_url: String,
    #[serde(default)]
    model: String,
}

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ConfigError {
    #[error("missing environment variable: {0}")]
    MissingEnv(&'static str),
    #[error("environment variable must not be empty: {0}")]
    EmptyValue(&'static str),
    #[error("invalid non-utf8 environment variable: {0}")]
    InvalidEnv(&'static str),
    #[error("failed to read DeepSeek config file {0}: {1}")]
    ConfigRead(PathBuf, String),
    #[error("invalid DeepSeek config JSON in {0}: {1}")]
    ConfigParse(PathBuf, String),
    #[error("DeepSeek config value must not be empty: {0} ({1})")]
    ConfigValueEmpty(&'static str, PathBuf),
}

impl ConfigError {
    fn with_path(self, path: &Path) -> Self {
        match self {
            Self::EmptyValue(field) => Self::ConfigValueEmpty(field, path.to_path_buf()),
            other => other,
        }
    }
}
