use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::template::schema::AllowRule;

const APP_NAME: &str = "earl";

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Config {
    #[serde(default)]
    pub search: SearchConfig,
    #[serde(default)]
    pub auth: AuthConfig,
    #[serde(default)]
    pub network: NetworkConfig,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SearchConfig {
    #[serde(default = "default_top_k")]
    pub top_k: usize,
    #[serde(default = "default_rerank_k")]
    pub rerank_k: usize,
    #[serde(default)]
    pub local: LocalSearchConfig,
    #[serde(default)]
    pub remote: RemoteSearchConfig,
}

impl Default for SearchConfig {
    fn default() -> Self {
        Self {
            top_k: default_top_k(),
            rerank_k: default_rerank_k(),
            local: LocalSearchConfig::default(),
            remote: RemoteSearchConfig::default(),
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LocalSearchConfig {
    #[serde(default = "default_embedding_model")]
    pub embedding_model: String,
    #[serde(default = "default_reranker_model")]
    pub reranker_model: String,
}

impl Default for LocalSearchConfig {
    fn default() -> Self {
        Self {
            embedding_model: default_embedding_model(),
            reranker_model: default_reranker_model(),
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RemoteSearchConfig {
    #[serde(default)]
    pub enabled: bool,
    pub base_url: Option<String>,
    pub api_key_secret: Option<String>,
    #[serde(default = "default_embeddings_path")]
    pub embeddings_path: String,
    #[serde(default = "default_rerank_path")]
    pub rerank_path: String,
    #[serde(default = "default_true")]
    pub openai_compatible: bool,
    #[serde(default = "default_remote_timeout_ms")]
    pub timeout_ms: u64,
}

impl Default for RemoteSearchConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            base_url: None,
            api_key_secret: None,
            embeddings_path: default_embeddings_path(),
            rerank_path: default_rerank_path(),
            openai_compatible: true,
            timeout_ms: default_remote_timeout_ms(),
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct AuthConfig {
    #[serde(default)]
    pub profiles: BTreeMap<String, OAuthProfile>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct NetworkConfig {
    #[serde(default)]
    pub allow: Vec<AllowRule>,
    #[serde(default)]
    pub allow_cli_commands: Vec<String>,
    #[serde(default)]
    pub proxy_profiles: BTreeMap<String, ProxyProfile>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ProxyProfile {
    pub url: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OAuthProfile {
    pub flow: OAuthFlow,
    pub client_id: String,
    pub client_secret_key: Option<String>,
    pub issuer: Option<String>,
    pub authorization_url: Option<String>,
    pub token_url: Option<String>,
    pub device_authorization_url: Option<String>,
    pub redirect_url: Option<String>,
    #[serde(default)]
    pub scopes: Vec<String>,
    #[serde(default)]
    pub use_auth_request_body: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum OAuthFlow {
    AuthCodePkce,
    DeviceCode,
    ClientCredentials,
}

pub fn load_config() -> Result<Config> {
    let path = config_file_path();
    if !path.exists() {
        return Ok(Config::default());
    }
    let raw = fs::read_to_string(&path)
        .with_context(|| format!("failed reading config file {}", path.display()))?;
    let cfg: Config =
        toml::from_str(&raw).with_context(|| format!("invalid config TOML {}", path.display()))?;
    Ok(cfg)
}

pub fn ensure_runtime_dirs() -> Result<()> {
    for path in [
        config_dir(),
        state_dir(),
        cache_dir(),
        global_templates_dir(),
    ] {
        fs::create_dir_all(&path)
            .with_context(|| format!("failed creating directory {}", path.display()))?;
    }
    Ok(())
}

pub fn local_templates_dir(cwd: &Path) -> PathBuf {
    cwd.join("templates")
}

pub fn global_templates_dir() -> PathBuf {
    config_dir().join("templates")
}

pub fn config_dir() -> PathBuf {
    home_dir().join(".config").join(APP_NAME)
}

pub fn config_file_path() -> PathBuf {
    config_dir().join("config.toml")
}

pub fn state_dir() -> PathBuf {
    home_dir().join(".local").join("state").join(APP_NAME)
}

pub fn cache_dir() -> PathBuf {
    home_dir().join(".cache").join(APP_NAME)
}

pub fn secrets_index_path() -> PathBuf {
    state_dir().join("secrets-index.json")
}

pub fn search_index_path() -> PathBuf {
    cache_dir().join("search-index-v1.json")
}

fn home_dir() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."))
}

fn default_top_k() -> usize {
    40
}

fn default_rerank_k() -> usize {
    10
}

fn default_embedding_model() -> String {
    "BGESmallENV15Q".to_string()
}

fn default_reranker_model() -> String {
    "JINARerankerV1TurboEn".to_string()
}

fn default_true() -> bool {
    true
}

fn default_embeddings_path() -> String {
    "/embeddings".to_string()
}

fn default_rerank_path() -> String {
    "/rerank".to_string()
}

fn default_remote_timeout_ms() -> u64 {
    10_000
}

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

    #[test]
    fn deserialize_ignores_unknown_legacy_table() {
        let loaded: Config = toml::from_str(
            r#"
[search]
top_k = 11
rerank_k = 9

[legacy_template_sources."acme/tools"]
url = "https://example.com/templates/github.hcl"
"#,
        )
        .unwrap();
        assert_eq!(loaded.search.top_k, 11);
        assert_eq!(loaded.search.rerank_k, 9);
    }
}
