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

use anyhow::{Context, Result, anyhow};
pub(crate) use fabro_client::ServerTarget;
pub(crate) use fabro_config::user::{active_settings_path, default_storage_dir};
use fabro_config::user::{default_settings_path, default_socket_path};
use fabro_config::{
    CliLayer, LogFilter, ParseError, RunSettingsBuilder, ServerSettingsBuilder, UserSettingsBuilder,
};
use fabro_static::EnvVars;
use fabro_types::settings::RunNamespace;
use fabro_types::settings::cli::CliTargetSettings;
use fabro_types::settings::server::LogDestination;
use fabro_types::{ServerSettings, UserSettings};
use fabro_util::error::SharedError;
use fabro_util::version::FABRO_VERSION;
use toml_edit::{DocumentMut, Item, Table, value};
use tracing::debug;

use crate::args::ServerTargetArgs;

pub(crate) struct LoadedSettings {
    pub(crate) storage_dir:            PathBuf,
    pub(crate) config_log_level:       Option<LogFilter>,
    pub(crate) config_log_destination: Option<LogDestination>,
    pub(crate) run_settings:           std::result::Result<RunNamespace, SharedError>,
    pub(crate) server_settings:        std::result::Result<ServerSettings, SharedError>,
    pub(crate) user_settings:          UserSettings,
}

pub(crate) fn load_resolved_settings(
    config_path: Option<&Path>,
    storage_dir: Option<&Path>,
    cli_layer: Option<&CliLayer>,
) -> anyhow::Result<LoadedSettings> {
    let document = load_settings_document(config_path)?;
    let storage_override = storage_dir.map(Path::to_path_buf);
    let storage_dir = storage_dir_from_document(&document, storage_dir);
    let pre_tracing_config = pre_tracing_config_from_document(&document)?;
    let run_settings = load_run_settings(config_path).map_err(SharedError::new);
    let server_settings = load_server_settings(config_path)
        .map(|settings| match storage_override.as_deref() {
            Some(dir) => settings.with_storage_override(dir),
            None => settings,
        })
        .map_err(SharedError::new);
    let user_settings = load_user_settings(config_path, cli_layer)?;

    Ok(LoadedSettings {
        storage_dir,
        config_log_level: pre_tracing_config.log_level,
        config_log_destination: pre_tracing_config.log_destination,
        run_settings,
        server_settings,
        user_settings,
    })
}

fn load_settings_document(config_path: Option<&Path>) -> anyhow::Result<toml::Value> {
    load_settings_document_with_lookup(config_path, process_env_var_os)
}

#[expect(
    clippy::disallowed_methods,
    reason = "sync settings load during CLI startup; not on a Tokio path"
)]
fn load_settings_document_with_lookup(
    config_path: Option<&Path>,
    lookup: impl Fn(&str) -> Option<std::ffi::OsString>,
) -> anyhow::Result<toml::Value> {
    let config_path = config_path
        .map(Path::to_path_buf)
        .or_else(|| lookup(EnvVars::FABRO_CONFIG).map(PathBuf::from));

    let path = if let Some(path) = config_path {
        path
    } else {
        let default_path = default_settings_path();
        if !default_path.is_file() {
            return Ok(toml::Value::Table(toml::Table::new()));
        }
        default_path
    };

    let contents = std::fs::read_to_string(&path)
        .map_err(|source| fabro_config::Error::read_file(&path, source))?;
    let table: toml::Table = toml::from_str(&contents).map_err(|source| {
        fabro_config::Error::parse_file(
            "Failed to parse settings file",
            &path,
            ParseError::Toml(source.to_string()),
        )
    })?;
    Ok(toml::Value::Table(table))
}

fn load_run_settings(config_path: Option<&Path>) -> anyhow::Result<RunNamespace> {
    let catalog = fabro_environment::seeded_catalog_layer();
    Ok(match config_path {
        Some(path) => RunSettingsBuilder::load_from_with_catalog(path, catalog)?,
        None => RunSettingsBuilder::load_default_with_catalog(catalog)?,
    })
}

fn load_server_settings(config_path: Option<&Path>) -> anyhow::Result<ServerSettings> {
    Ok(match config_path {
        Some(path) => ServerSettingsBuilder::load_from(path)?,
        None => ServerSettingsBuilder::load_default()?,
    })
}

fn load_user_settings(
    config_path: Option<&Path>,
    cli_layer: Option<&CliLayer>,
) -> anyhow::Result<UserSettings> {
    Ok(match (config_path, cli_layer) {
        (Some(path), Some(cli_layer)) => {
            UserSettingsBuilder::load_from_with_cli_overrides(path, cli_layer)?
        }
        (Some(path), None) => UserSettingsBuilder::load_from(path)?,
        (None, Some(cli_layer)) => UserSettingsBuilder::load_default_with_cli_overrides(cli_layer)?,
        (None, None) => UserSettingsBuilder::load_default()?,
    })
}

struct PreTracingConfig {
    log_level:       Option<LogFilter>,
    log_destination: Option<LogDestination>,
}

fn pre_tracing_config_from_document(document: &toml::Value) -> Result<PreTracingConfig> {
    Ok(PreTracingConfig {
        log_level:       log_filter_at_path(document, &["server", "logging", "level"])?,
        log_destination: log_destination_at_path(document, &["server", "logging", "destination"])?,
    })
}

fn log_filter_at_path(document: &toml::Value, path: &[&str]) -> Result<Option<LogFilter>> {
    let Some(value) = value_at_path(document, path) else {
        return Ok(None);
    };
    let raw = value
        .as_str()
        .ok_or_else(|| anyhow!("{} must be a string", path.join(".")))?;
    LogFilter::parse(raw)
        .with_context(|| format!("invalid {} `{raw}`", path.join(".")))
        .map(Some)
}

fn log_destination_at_path(
    document: &toml::Value,
    path: &[&str],
) -> Result<Option<LogDestination>> {
    let Some(value) = value_at_path(document, path) else {
        return Ok(None);
    };
    let raw = value
        .as_str()
        .ok_or_else(|| anyhow!("{} must be a string", path.join(".")))?;
    raw.parse::<LogDestination>()
        .with_context(|| {
            format!(
                "invalid {} `{raw}`; expected `file` or `stdout`",
                path.join(".")
            )
        })
        .map(Some)
}

pub(crate) fn storage_dir_from_document(
    document: &toml::Value,
    storage_dir: Option<&Path>,
) -> PathBuf {
    if let Some(dir) = storage_dir {
        return dir.to_path_buf();
    }

    // `server.storage.root` is plain control-plane config and does not
    // interpolate; the FABRO_STORAGE_DIR-backed `storage_dir` argument above is
    // the deployment-time override.
    let storage_root = string_at_path(document, &["server", "storage", "root"])
        .unwrap_or_else(|| default_storage_dir().to_string_lossy().into_owned());
    PathBuf::from(storage_root)
}

#[expect(
    clippy::disallowed_methods,
    reason = "CLI settings loading owns the process-env facade for config path lookup."
)]
fn process_env_var_os(name: &str) -> Option<std::ffi::OsString> {
    std::env::var_os(name)
}

fn string_at_path(document: &toml::Value, path: &[&str]) -> Option<String> {
    value_at_path(document, path).and_then(|value| value.as_str().map(str::to_owned))
}

fn value_at_path<'a>(document: &'a toml::Value, path: &[&str]) -> Option<&'a toml::Value> {
    let mut current = document;
    for segment in path {
        current = current.get(*segment)?;
    }
    Some(current)
}

fn configured_server_target(settings: &UserSettings) -> Result<Option<ServerTarget>> {
    match settings.cli.target.as_ref() {
        Some(CliTargetSettings::Http { url }) => ServerTarget::http_url(url).map(Some),
        Some(CliTargetSettings::Unix { path }) => ServerTarget::unix_socket_path(path).map(Some),
        None => Ok(None),
    }
}

pub(crate) fn default_server_target() -> ServerTarget {
    ServerTarget::unix_socket_path(default_socket_path()).expect("default socket path is absolute")
}

fn parse_server_target(value: &str) -> Result<ServerTarget> {
    ServerTarget::from_str(value)
}

fn explicit_server_target(args: &ServerTargetArgs) -> Result<Option<ServerTarget>> {
    args.as_deref().map(parse_server_target).transpose()
}

pub(crate) fn resolve_nondefault_server_target(
    args: &ServerTargetArgs,
    settings: &UserSettings,
) -> Result<Option<ServerTarget>> {
    Ok(explicit_server_target(args)?.or(configured_server_target(settings)?))
}

pub(crate) fn resolve_server_target(
    args: &ServerTargetArgs,
    settings: &UserSettings,
) -> Result<ServerTarget> {
    Ok(resolve_nondefault_server_target(args, settings)?.unwrap_or_else(default_server_target))
}

#[expect(
    clippy::disallowed_methods,
    reason = "CLI auth/login updates the user settings file synchronously after successful login."
)]
pub(crate) fn configure_cli_target_if_missing(
    config_path: &Path,
    settings: &UserSettings,
    target: &ServerTarget,
) -> Result<bool> {
    if settings.cli.target.is_some() {
        return Ok(false);
    }

    let mut document = read_settings_document_for_write(config_path)?;
    if document
        .get("cli")
        .and_then(Item::as_table)
        .is_some_and(|cli| cli.get("target").is_some_and(|target| !target.is_none()))
    {
        return Ok(false);
    }

    if document.get("_version").is_none() {
        document["_version"] = value(1);
    }

    let cli = ensure_document_table(&mut document, "cli")?;
    cli.insert("target", Item::Table(cli_target_table(target)));

    if let Some(parent) = config_path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("failed to create {}", parent.display()))?;
    }
    std::fs::write(config_path, document.to_string())
        .with_context(|| format!("failed to write {}", config_path.display()))?;

    Ok(true)
}

#[expect(
    clippy::disallowed_methods,
    reason = "CLI auth/login reads the user settings file synchronously before writing it."
)]
fn read_settings_document_for_write(config_path: &Path) -> Result<DocumentMut> {
    if !config_path.exists() {
        return Ok(DocumentMut::new());
    }

    let contents = std::fs::read_to_string(config_path)
        .with_context(|| format!("failed to read {}", config_path.display()))?;
    contents
        .parse::<DocumentMut>()
        .with_context(|| format!("failed to parse {}", config_path.display()))
}

fn ensure_document_table<'a>(document: &'a mut DocumentMut, key: &str) -> Result<&'a mut Table> {
    if document.get(key).is_none() {
        let mut table = Table::new();
        table.set_implicit(true);
        document[key] = Item::Table(table);
    }
    document[key]
        .as_table_mut()
        .ok_or_else(|| anyhow!("settings.toml [{key}] is not a table"))
}

fn cli_target_table(target: &ServerTarget) -> Table {
    let mut table = Table::new();
    if let Some(url) = target.as_http_url() {
        table.insert("type", value("http"));
        table.insert("url", value(url));
    } else if let Some(path) = target.as_unix_socket_path() {
        table.insert("type", value("unix"));
        table.insert("path", value(path.display().to_string()));
    }
    table
}

pub(crate) fn exec_server_target(args: &ServerTargetArgs) -> Result<Option<ServerTarget>> {
    let target = explicit_server_target(args)?;
    debug!(?target, "Resolved exec server target");
    Ok(target)
}

pub(crate) fn cli_http_client_builder() -> fabro_http::HttpClientBuilder {
    fabro_http::HttpClientBuilder::new().user_agent(format!("fabro-cli/{FABRO_VERSION}"))
}

#[cfg(test)]
pub(crate) fn load_resolved_settings_from_toml(
    source: &str,
    storage_dir: Option<&Path>,
    cli_layer: Option<&CliLayer>,
) -> anyhow::Result<LoadedSettings> {
    let document: toml::Value = toml::from_str(source).context("failed to parse settings file")?;
    let storage_override = storage_dir.map(Path::to_path_buf);
    let storage_dir = storage_dir_from_document(&document, storage_dir);
    let pre_tracing_config = pre_tracing_config_from_document(&document)?;
    let run_settings = RunSettingsBuilder::from_toml_with_catalog(
        source,
        fabro_environment::seeded_catalog_layer(),
    )
    .map_err(|err| SharedError::new(anyhow::Error::new(err)));
    let server_settings = ServerSettingsBuilder::from_toml(source)
        .map(|settings| match storage_override.as_deref() {
            Some(dir) => settings.with_storage_override(dir),
            None => settings,
        })
        .map_err(|err| SharedError::new(anyhow::Error::new(err)));
    let user_settings = match cli_layer {
        Some(cli_layer) => UserSettingsBuilder::from_toml_with_cli_overrides(source, cli_layer)?,
        None => UserSettingsBuilder::from_toml(source)?,
    };

    Ok(LoadedSettings {
        storage_dir,
        config_log_level: pre_tracing_config.log_level,
        config_log_destination: pre_tracing_config.log_destination,
        run_settings,
        server_settings,
        user_settings,
    })
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use fabro_config::UserSettingsBuilder;
    use fabro_config::user::default_storage_dir;
    use fabro_types::UserSettings;

    use super::*;
    use crate::args::ServerTargetArgs;

    fn server_target_args(value: Option<&str>) -> ServerTargetArgs {
        ServerTargetArgs {
            server: value.map(str::to_string),
        }
    }

    fn parse_user_settings(source: &str) -> UserSettings {
        UserSettingsBuilder::from_toml(source).expect("fixture should resolve")
    }

    #[test]
    fn exec_has_no_server_target_by_default() {
        assert_eq!(exec_server_target(&server_target_args(None)).unwrap(), None);
    }

    #[test]
    fn exec_uses_cli_server_target() {
        assert_eq!(
            exec_server_target(&server_target_args(Some("https://cli.example.com"))).unwrap(),
            Some(ServerTarget::http_url("https://cli.example.com").unwrap())
        );
    }

    #[test]
    fn exec_supports_explicit_unix_socket_target() {
        assert_eq!(
            exec_server_target(&server_target_args(Some("/tmp/fabro.sock"))).unwrap(),
            Some(ServerTarget::unix_socket_path("/tmp/fabro.sock").unwrap())
        );
    }

    #[test]
    fn exec_ignores_configured_server_target_without_cli_override() {
        assert_eq!(exec_server_target(&server_target_args(None)).unwrap(), None);
    }

    #[test]
    fn resolve_server_target_uses_configured_server_target() {
        let settings = parse_user_settings(
            r#"
_version = 1

[cli.target]
type = "http"
url = "https://config.example.com"
"#,
        );
        assert_eq!(
            resolve_server_target(&server_target_args(None), &settings).unwrap(),
            ServerTarget::http_url("https://config.example.com").unwrap()
        );
    }

    #[test]
    fn resolve_server_target_explicit_target_overrides_config_target() {
        let settings = parse_user_settings(
            r#"
_version = 1

[cli.target]
type = "http"
url = "https://config.example.com"
"#,
        );
        assert_eq!(
            resolve_server_target(
                &server_target_args(Some("https://cli.example.com")),
                &settings
            )
            .unwrap(),
            ServerTarget::http_url("https://cli.example.com").unwrap()
        );
    }

    #[test]
    fn resolve_server_target_defaults_to_default_unix_socket_target() {
        let settings = UserSettings::default();
        assert_eq!(
            resolve_server_target(&server_target_args(None), &settings).unwrap(),
            ServerTarget::unix_socket_path(dirs::home_dir().unwrap().join(".fabro/fabro.sock"))
                .unwrap()
        );
    }

    #[test]
    fn explicit_server_target_overrides_config_target() {
        let settings = parse_user_settings(
            r#"
_version = 1

[cli.target]
type = "http"
url = "https://config.example.com"
"#,
        );
        assert_eq!(
            resolve_server_target(
                &server_target_args(Some("https://cli.example.com")),
                &settings
            )
            .unwrap(),
            ServerTarget::http_url("https://cli.example.com").unwrap()
        );
    }

    #[test]
    fn invalid_server_target_is_rejected() {
        let error = exec_server_target(&server_target_args(Some("fabro.internal"))).unwrap_err();
        assert_eq!(
            error.to_string(),
            "server target must be an http(s) URL or absolute Unix socket path"
        );
    }

    #[test]
    #[expect(
        clippy::disallowed_methods,
        reason = "unit test writes a temporary settings fixture with sync std::fs"
    )]
    fn configure_cli_target_creates_settings_file_when_missing() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join(".fabro").join("settings.toml");
        let settings = UserSettings::default();
        let target = ServerTarget::http_url("http://127.0.0.1:32276").unwrap();

        assert!(configure_cli_target_if_missing(&config_path, &settings, &target).unwrap());

        let contents = std::fs::read_to_string(&config_path).unwrap();
        insta::assert_snapshot!(contents, @r#"
        _version = 1

        [cli.target]
        type = "http"
        url = "http://127.0.0.1:32276"
        "#);
        let settings = UserSettingsBuilder::load_from(&config_path).unwrap();
        assert_eq!(
            resolve_server_target(&server_target_args(None), &settings).unwrap(),
            target
        );
    }

    #[test]
    #[expect(
        clippy::disallowed_methods,
        reason = "unit test writes a temporary settings fixture with sync std::fs"
    )]
    fn configure_cli_target_does_not_overwrite_existing_target() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join("settings.toml");
        let existing = r#"
_version = 1

[cli.target]
type = "http"
url = "https://configured.example.com"
"#;
        std::fs::write(&config_path, existing).unwrap();
        let settings = UserSettingsBuilder::load_from(&config_path).unwrap();
        let target = ServerTarget::http_url("https://new.example.com").unwrap();

        assert!(!configure_cli_target_if_missing(&config_path, &settings, &target).unwrap());

        assert_eq!(std::fs::read_to_string(&config_path).unwrap(), existing);
    }

    #[test]
    fn storage_dir_defaults_without_server_auth_methods() {
        let document = toml::Value::Table(toml::Table::new());

        assert_eq!(
            storage_dir_from_document(&document, None),
            default_storage_dir()
        );
    }

    #[test]
    fn storage_dir_uses_explicit_server_storage_root() {
        let document: toml::Value = toml::from_str(
            r#"
_version = 1

[server.storage]
root = "/srv/fabro"
"#,
        )
        .expect("fixture should parse");

        assert_eq!(
            storage_dir_from_document(&document, None),
            PathBuf::from("/srv/fabro")
        );
    }

    #[test]
    fn storage_dir_keeps_template_token_literal() {
        // server.storage.root is plain control-plane config now: a `{{ env.* }}`
        // token is used verbatim, never resolved. Deployment-time overrides go
        // through the FABRO_STORAGE_DIR-backed `storage_dir` argument instead.
        let document: toml::Value = toml::from_str(
            r#"
_version = 1

[server.storage]
root = "{{ env.FABRO_STORAGE_ROOT }}"
"#,
        )
        .expect("fixture should parse");

        assert_eq!(
            storage_dir_from_document(&document, None),
            PathBuf::from("{{ env.FABRO_STORAGE_ROOT }}")
        );
    }

    #[test]
    #[expect(
        clippy::disallowed_methods,
        reason = "unit test writes a temporary settings fixture with sync std::fs::write"
    )]
    fn load_settings_document_uses_fabro_config_env_for_storage_root() {
        let dir = tempfile::tempdir().unwrap();
        let config_path = dir.path().join("settings.toml");
        std::fs::write(
            &config_path,
            r#"
_version = 1

[server.storage]
root = "/srv/fabro"
"#,
        )
        .unwrap();

        let document = load_settings_document_with_lookup(None, |_| {
            Some(config_path.clone().into_os_string())
        })
        .expect("settings document should load");

        assert_eq!(
            storage_dir_from_document(&document, None),
            PathBuf::from("/srv/fabro")
        );
    }
}
