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

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

use crate::runtime::RuntimeProfile;

pub use zeroclaw::config::SkillsPromptInjectionMode as SkillsPromptMode;

const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com";
const DEFAULT_DEEPSEEK_MODEL: &str = "deepseek-chat";
const DEFAULT_PROVIDER_ID: &str = "deepseek";
const DIRECT_SUBMIT_PROVIDER_ID: &str = "direct-submit";
const DIRECT_SUBMIT_BASE_URL: &str = "http://127.0.0.1/direct-submit";
const DIRECT_SUBMIT_MODEL: &str = "direct-submit-placeholder-model";
const DIRECT_SUBMIT_API_KEY: &str = "direct-submit-placeholder-key";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlannerMode {
    ZeroclawPlanFirst,
    LegacyDeterministic,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserBackend {
    SuperRpa,
    AgentBrowser,
    RustNative,
    ComputerUse,
    Auto,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OfficeBackend {
    OpenXml,
    Disabled,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderSettings {
    pub id: String,
    pub provider: String,
    pub api_key: String,
    pub base_url: Option<String>,
    pub model: String,
    pub api_path: Option<String>,
    pub wire_api: Option<String>,
    pub requires_openai_auth: bool,
}

impl ProviderSettings {
    fn from_legacy_deepseek(
        api_key: String,
        base_url: String,
        model: String,
    ) -> Result<Self, ConfigError> {
        let api_key = normalize_required_value("DEEPSEEK_API_KEY", api_key)?;
        let base_url = normalize_base_url(base_url);
        let model = normalize_model(model);

        Ok(Self {
            id: DEFAULT_PROVIDER_ID.to_string(),
            provider: DEFAULT_PROVIDER_ID.to_string(),
            api_key,
            base_url: Some(base_url),
            model,
            api_path: None,
            wire_api: None,
            requires_openai_auth: false,
        })
    }

    fn direct_submit_placeholder() -> Self {
        Self {
            id: DIRECT_SUBMIT_PROVIDER_ID.to_string(),
            provider: DIRECT_SUBMIT_PROVIDER_ID.to_string(),
            api_key: DIRECT_SUBMIT_API_KEY.to_string(),
            base_url: Some(DIRECT_SUBMIT_BASE_URL.to_string()),
            model: DIRECT_SUBMIT_MODEL.to_string(),
            api_path: None,
            wire_api: None,
            requires_openai_auth: false,
        }
    }

    fn from_raw(raw: RawProviderSettings) -> Result<Self, ConfigError> {
        let id = raw.id.trim().to_string();
        if id.is_empty() {
            return Err(ConfigError::InvalidValue(
                "providers[].id",
                "must not be empty".to_string(),
            ));
        }

        let api_key = normalize_required_value("providers[].apiKey", raw.api_key)?;
        let model = normalize_required_value("providers[].model", raw.model)?;
        let base_url = normalize_optional_value(raw.base_url);
        let provider = normalize_optional_value(raw.provider)
            .or_else(|| base_url.as_ref().map(|url| format!("custom:{url}")))
            .ok_or_else(|| {
                ConfigError::InvalidValue(
                    "providers[].provider",
                    format!("provider {} must define provider or baseUrl", id),
                )
            })?;

        Ok(Self {
            id,
            provider,
            api_key,
            base_url,
            model,
            api_path: normalize_optional_value(raw.api_path),
            wire_api: normalize_optional_value(raw.wire_api),
            requires_openai_auth: raw.requires_openai_auth,
        })
    }
}

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

impl DeepSeekSettings {
    pub fn from_env() -> Result<Self, ConfigError> {
        Ok(Self::from(&SgClawSettings::from_env()?))
    }

    pub fn load(config_path: Option<&Path>) -> Result<Option<Self>, ConfigError> {
        SgClawSettings::load(config_path)
            .map(|settings| settings.map(|settings| Self::from(&settings)))
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SgClawSettings {
    pub provider_api_key: String,
    pub provider_base_url: String,
    pub provider_model: String,
    pub skills_dir: Option<PathBuf>,
    pub direct_submit_skill: Option<String>,
    pub skills_prompt_mode: SkillsPromptMode,
    pub runtime_profile: RuntimeProfile,
    pub planner_mode: PlannerMode,
    pub providers: Vec<ProviderSettings>,
    pub active_provider: String,
    pub browser_backend: BrowserBackend,
    pub office_backend: OfficeBackend,
    pub browser_ws_url: Option<String>,
    pub service_ws_listen_addr: Option<String>,
}

impl SgClawSettings {
    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()
    }

    pub fn from_legacy_deepseek_fields(
        api_key: String,
        base_url: String,
        model: String,
        skills_dir: Option<PathBuf>,
    ) -> Result<Self, ConfigError> {
        Self::new(
            api_key,
            base_url,
            model,
            skills_dir,
            None,
            None,
            None,
            None,
            Vec::new(),
            None,
            None,
            None,
            None,
            None,
        )
    }

    pub fn active_provider_settings(&self) -> &ProviderSettings {
        self.providers
            .iter()
            .find(|provider| provider.id == self.active_provider)
            .expect("active_provider should always resolve to a configured provider")
    }

    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,
            None,
            None,
            None,
            None,
            None,
            Vec::new(),
            None,
            None,
            None,
            None,
            None,
        )?))
    }

    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: RawSgClawSettings = serde_json::from_str(&raw)
            .map_err(|err| ConfigError::ConfigParse(path.to_path_buf(), err.to_string()))?;
        let config_dir = path.parent().unwrap_or_else(|| Path::new("."));
        let runtime_profile = config
            .runtime_profile
            .as_deref()
            .map(parse_runtime_profile)
            .transpose()
            .map_err(|value| {
                ConfigError::ConfigParse(
                    path.to_path_buf(),
                    format!("invalid runtimeProfile: {value}"),
                )
            })?;
        let skills_prompt_mode = config
            .skills_prompt_mode
            .as_deref()
            .map(parse_skills_prompt_mode)
            .transpose()
            .map_err(|value| {
                ConfigError::ConfigParse(
                    path.to_path_buf(),
                    format!("invalid skillsPromptMode: {value}"),
                )
            })?;
        let planner_mode = config
            .planner_mode
            .as_deref()
            .map(parse_planner_mode)
            .transpose()
            .map_err(|value| {
                ConfigError::ConfigParse(
                    path.to_path_buf(),
                    format!("invalid plannerMode: {value}"),
                )
            })?;
        let browser_backend = config
            .browser_backend
            .as_deref()
            .map(parse_browser_backend)
            .transpose()
            .map_err(|value| {
                ConfigError::ConfigParse(
                    path.to_path_buf(),
                    format!("invalid browserBackend: {value}"),
                )
            })?;
        let office_backend = config
            .office_backend
            .as_deref()
            .map(parse_office_backend)
            .transpose()
            .map_err(|value| {
                ConfigError::ConfigParse(
                    path.to_path_buf(),
                    format!("invalid officeBackend: {value}"),
                )
            })?;
        let providers = config
            .providers
            .into_iter()
            .map(ProviderSettings::from_raw)
            .collect::<Result<Vec<_>, _>>()
            .map_err(|err| err.with_path(path))?;

        Self::new(
            config.api_key,
            config.base_url,
            config.model,
            resolve_configured_skills_dir(config.skills_dir, config_dir),
            config.direct_submit_skill,
            skills_prompt_mode,
            runtime_profile,
            planner_mode,
            providers,
            config.active_provider,
            browser_backend,
            office_backend,
            config.browser_ws_url,
            config.service_ws_listen_addr,
        )
        .map_err(|err| err.with_path(path))
    }

    fn new(
        api_key: String,
        base_url: String,
        model: String,
        skills_dir: Option<PathBuf>,
        direct_submit_skill: Option<String>,
        skills_prompt_mode: Option<SkillsPromptMode>,
        runtime_profile: Option<RuntimeProfile>,
        planner_mode: Option<PlannerMode>,
        providers: Vec<ProviderSettings>,
        active_provider: Option<String>,
        browser_backend: Option<BrowserBackend>,
        office_backend: Option<OfficeBackend>,
        browser_ws_url: Option<String>,
        service_ws_listen_addr: Option<String>,
    ) -> Result<Self, ConfigError> {
        let direct_submit_skill = normalize_direct_submit_skill(direct_submit_skill)?;
        let providers = if providers.is_empty() {
            if direct_submit_skill.is_some() {
                vec![ProviderSettings::direct_submit_placeholder()]
            } else {
                vec![ProviderSettings::from_legacy_deepseek(
                    api_key, base_url, model,
                )?]
            }
        } else {
            providers
        };
        let active_provider =
            normalize_optional_value(active_provider).unwrap_or_else(|| providers[0].id.clone());
        let active_provider_settings = providers
            .iter()
            .find(|provider| provider.id == active_provider)
            .ok_or_else(|| {
                ConfigError::InvalidValue(
                    "activeProvider",
                    format!("unknown provider id: {active_provider}"),
                )
            })?;

        Ok(Self {
            provider_api_key: active_provider_settings.api_key.clone(),
            provider_base_url: active_provider_settings
                .base_url
                .clone()
                .unwrap_or_default(),
            provider_model: active_provider_settings.model.clone(),
            skills_dir,
            direct_submit_skill,
            skills_prompt_mode: skills_prompt_mode.unwrap_or(SkillsPromptMode::Compact),
            runtime_profile: runtime_profile.unwrap_or(RuntimeProfile::BrowserAttached),
            planner_mode: planner_mode.unwrap_or(PlannerMode::ZeroclawPlanFirst),
            providers,
            active_provider,
            browser_backend: browser_backend.unwrap_or(BrowserBackend::SuperRpa),
            office_backend: office_backend.unwrap_or(OfficeBackend::OpenXml),
            browser_ws_url: normalize_optional_value(browser_ws_url),
            service_ws_listen_addr: normalize_optional_value(service_ws_listen_addr),
        })
    }
}

impl From<&SgClawSettings> for DeepSeekSettings {
    fn from(value: &SgClawSettings) -> Self {
        Self {
            api_key: value.provider_api_key.clone(),
            base_url: value.provider_base_url.clone(),
            model: value.provider_model.clone(),
            skills_dir: value.skills_dir.clone(),
        }
    }
}

impl From<&DeepSeekSettings> for SgClawSettings {
    fn from(value: &DeepSeekSettings) -> Self {
        Self::from_legacy_deepseek_fields(
            value.api_key.clone(),
            value.base_url.clone(),
            value.model.clone(),
            value.skills_dir.clone(),
        )
        .expect("DeepSeekSettings should already be validated")
    }
}

fn parse_runtime_profile(raw: &str) -> Result<RuntimeProfile, String> {
    let normalized = raw
        .trim()
        .chars()
        .filter(|ch| *ch != '_' && *ch != '-')
        .collect::<String>()
        .to_ascii_lowercase();

    match normalized.as_str() {
        "browserattached" => Ok(RuntimeProfile::BrowserAttached),
        "browserheavy" => Ok(RuntimeProfile::BrowserHeavy),
        "generalassistant" => Ok(RuntimeProfile::GeneralAssistant),
        _ => Err(raw.to_string()),
    }
}

fn parse_skills_prompt_mode(raw: &str) -> Result<SkillsPromptMode, String> {
    match raw.trim().to_ascii_lowercase().as_str() {
        "compact" => Ok(SkillsPromptMode::Compact),
        "full" => Ok(SkillsPromptMode::Full),
        _ => Err(raw.to_string()),
    }
}

fn parse_planner_mode(raw: &str) -> Result<PlannerMode, String> {
    let normalized = normalize_enum_token(raw);

    match normalized.as_str() {
        "zeroclawplanfirst" | "planfirst" | "plannerfirst" => Ok(PlannerMode::ZeroclawPlanFirst),
        "legacy" | "legacydeterministic" | "deterministic" => Ok(PlannerMode::LegacyDeterministic),
        _ => Err(raw.to_string()),
    }
}

fn parse_browser_backend(raw: &str) -> Result<BrowserBackend, String> {
    let normalized = normalize_enum_token(raw);

    match normalized.as_str() {
        "superrpa" | "superrpapipe" | "host" | "hostpipe" => Ok(BrowserBackend::SuperRpa),
        "agentbrowser" => Ok(BrowserBackend::AgentBrowser),
        "rustnative" => Ok(BrowserBackend::RustNative),
        "computeruse" => Ok(BrowserBackend::ComputerUse),
        "auto" => Ok(BrowserBackend::Auto),
        _ => Err(raw.to_string()),
    }
}

fn parse_office_backend(raw: &str) -> Result<OfficeBackend, String> {
    let normalized = normalize_enum_token(raw);

    match normalized.as_str() {
        "openxml" => Ok(OfficeBackend::OpenXml),
        "disabled" | "none" => Ok(OfficeBackend::Disabled),
        _ => Err(raw.to_string()),
    }
}

fn resolve_configured_skills_dir(raw: Option<String>, config_dir: &Path) -> Option<PathBuf> {
    raw.map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .map(|path| if path.is_absolute() { path } else { config_dir.join(path) })
}

fn normalize_required_value(field: &'static str, raw: String) -> Result<String, ConfigError> {
    let trimmed = raw.trim().to_string();
    if trimmed.is_empty() {
        return Err(ConfigError::EmptyValue(field));
    }
    Ok(trimmed)
}

fn normalize_optional_value(raw: Option<String>) -> Option<String> {
    raw.map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn normalize_direct_submit_skill(raw: Option<String>) -> Result<Option<String>, ConfigError> {
    let value = normalize_optional_value(raw);
    let Some(value) = value.as_deref() else {
        return Ok(None);
    };

    let Some((skill_name, tool_name)) = value.split_once('.') else {
        return Err(ConfigError::InvalidValue(
            "directSubmitSkill",
            format!("must use skill.tool format, got {value}"),
        ));
    };

    if skill_name.trim().is_empty() || tool_name.trim().is_empty() {
        return Err(ConfigError::InvalidValue(
            "directSubmitSkill",
            format!("must use skill.tool format, got {value}"),
        ));
    }

    Ok(Some(value.to_string()))
}

fn normalize_base_url(raw: String) -> String {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        DEFAULT_DEEPSEEK_BASE_URL.to_string()
    } else {
        trimmed.to_string()
    }
}

fn normalize_model(raw: String) -> String {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        DEFAULT_DEEPSEEK_MODEL.to_string()
    } else {
        trimmed.to_string()
    }
}

fn normalize_enum_token(raw: &str) -> String {
    raw.trim()
        .chars()
        .filter(|ch| *ch != '_' && *ch != '-')
        .collect::<String>()
        .to_ascii_lowercase()
}

#[derive(Debug, Deserialize)]
struct RawSgClawSettings {
    #[serde(rename = "apiKey", default)]
    api_key: String,
    #[serde(rename = "baseUrl", default)]
    base_url: String,
    #[serde(default)]
    model: String,
    #[serde(rename = "skillsDir", alias = "skills_dir", default)]
    skills_dir: Option<String>,
    #[serde(rename = "directSubmitSkill", alias = "direct_submit_skill", default)]
    direct_submit_skill: Option<String>,
    #[serde(rename = "skillsPromptMode", alias = "skills_prompt_mode", default)]
    skills_prompt_mode: Option<String>,
    #[serde(rename = "runtimeProfile", alias = "runtime_profile", default)]
    runtime_profile: Option<String>,
    #[serde(rename = "plannerMode", alias = "planner_mode", default)]
    planner_mode: Option<String>,
    #[serde(rename = "activeProvider", alias = "active_provider", default)]
    active_provider: Option<String>,
    #[serde(rename = "browserBackend", alias = "browser_backend", default)]
    browser_backend: Option<String>,
    #[serde(rename = "officeBackend", alias = "office_backend", default)]
    office_backend: Option<String>,
    #[serde(rename = "browserWsUrl", alias = "browser_ws_url", default)]
    browser_ws_url: Option<String>,
    #[serde(rename = "serviceWsListenAddr", alias = "service_ws_listen_addr", default)]
    service_ws_listen_addr: Option<String>,
    #[serde(default)]
    providers: Vec<RawProviderSettings>,
}

#[derive(Debug, Deserialize)]
struct RawProviderSettings {
    #[serde(default)]
    id: String,
    #[serde(default)]
    provider: Option<String>,
    #[serde(rename = "apiKey", default)]
    api_key: String,
    #[serde(rename = "baseUrl", default)]
    base_url: Option<String>,
    #[serde(default)]
    model: String,
    #[serde(rename = "apiPath", alias = "api_path", default)]
    api_path: Option<String>,
    #[serde(rename = "wireApi", alias = "wire_api", default)]
    wire_api: Option<String>,
    #[serde(rename = "requiresOpenaiAuth", alias = "requires_openai_auth", default)]
    requires_openai_auth: bool,
}

#[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),
    #[error("invalid config value for {0}: {1}")]
    InvalidValue(&'static str, String),
    #[error("invalid DeepSeek config value for {0}: {2} ({1})")]
    ConfigInvalidValue(&'static str, PathBuf, String),
}

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

impl BrowserBackend {
    pub fn zeroclaw_backend(self) -> Option<&'static str> {
        match self {
            Self::SuperRpa => None,
            Self::AgentBrowser => Some("agent_browser"),
            Self::RustNative => Some("rust_native"),
            Self::ComputerUse => Some("computer_use"),
            Self::Auto => Some("auto"),
        }
    }
}
