//! `provisioning-daemon` — HTTP+NATS server backed by the provisioning-core Registry.

use anyhow::{Context as _, Result};
use clap::Parser;
use platform_config::load_service_config_from_ncl_with_overrides;
use provisioning_core::{
    Environment,
    providers::hcloud::HcloudClient,
    sources::{NclCache, OrchestratorClient},
    Registry,
};
use provisioning_daemon::{
    AppState, auth::JwtConfig, events::EventBus, ontology::OntologyRenderer,
    ui::UiRenderer,
};
use std::sync::Arc;
use tokio::net::TcpListener;

#[derive(Parser)]
#[command(name = "provisioning-daemon", about = "Provisioning HTTP+NATS daemon", version)]
struct Cli {
    /// Config file path — overrides all other config sources.
    /// Supports .ncl, .toml, .json. Env: PROVISIONING_DAEMON_CONFIG
    #[arg(short = 'c', long, env = "PROVISIONING_DAEMON_CONFIG")]
    config: Option<std::path::PathBuf>,

    /// Config directory — searches for provisioning-daemon[.{mode}].ncl|toml.
    /// Env: PROVISIONING_CONFIG_DIR
    #[arg(long, env = "PROVISIONING_CONFIG_DIR")]
    config_dir: Option<std::path::PathBuf>,

    /// Deployment mode (solo, multiuser, enterprise).
    /// Selects provisioning-daemon.{mode}.ncl when searching config directories.
    /// Env: PROVISIONING_DAEMON_MODE
    #[arg(short = 'm', long, env = "PROVISIONING_DAEMON_MODE")]
    mode: Option<String>,

    // --- Runtime overrides (take priority over NCL config) ---

    #[arg(long, env = "PROVISIONING_DAEMON_BIND", default_value = "0.0.0.0:9014")]
    bind: String,

    #[arg(long, env = "ORCHESTRATOR_URL", default_value = "http://localhost:9011")]
    orchestrator_url: String,

    #[arg(long, env = "NATS_URL", default_value = "nats://127.0.0.1:4222")]
    nats_url: String,

    /// HS256 JWT secret (omit to disable JWT — solo/dev mode).
    #[arg(long, env = "PROVISIONING_JWT_SECRET")]
    jwt_secret: Option<String>,

    /// Config paths to watch for live-reload (comma-separated).
    #[arg(long, env = "PROVISIONING_WATCH_PATHS", value_delimiter = ',')]
    watch_paths: Vec<std::path::PathBuf>,

    /// Directory of Tera templates for `/api/v1/ontology/render/*`.
    #[arg(long, env = "PROVISIONING_ONTOLOGY_TEMPLATES")]
    ontology_templates: Option<std::path::PathBuf>,

    /// Project name exposed in the ontology render context.
    #[arg(long, env = "PROVISIONING_PROJECT_NAME", default_value = "provisioning")]
    project_name: String,

    /// Root of the provisioning repo on disk — enables /ui/ontology and /ui/modes.
    #[arg(long, env = "PROVISIONING_PROJECT_ROOT")]
    project_root: Option<std::path::PathBuf>,

    /// Override directory for Tera SSR UI templates (default: embedded).
    #[arg(long, env = "PROVISIONING_UI_TEMPLATES_DIR")]
    ui_templates_dir: Option<std::path::PathBuf>,

    /// Nickel import path for `nickel export` calls (NICKEL_IMPORT_PATH).
    #[arg(long, env = "NICKEL_IMPORT_PATH")]
    nickel_import_path: Option<String>,

    /// Path to the `provisioning` CLI binary (for /ui/modes run).
    #[arg(long, env = "PROVISIONING_BIN", default_value = "provisioning")]
    provisioning_bin: String,

    /// Workspaces root directory (default: {project-root}/../workspaces).
    #[arg(long, env = "PROVISIONING_WORKSPACES_ROOT")]
    workspaces_root: Option<std::path::PathBuf>,

    /// Extensions root directory (default: {project-root}/extensions).
    #[arg(long, env = "PROVISIONING_EXTENSIONS_ROOT")]
    extensions_root: Option<std::path::PathBuf>,

    /// Static password for UI login in solo/dev mode.
    #[arg(long, env = "PROVISIONING_UI_PASSWORD")]
    ui_password: Option<String>,

    /// Control-center URL for delegated auth (POST {url}/api/v1/auth/verify).
    #[arg(long, env = "PROVISIONING_CONTROL_CENTER_URL")]
    control_center_url: Option<String>,
}

#[derive(Debug, serde::Deserialize)]
struct ServerConfig {
    #[serde(default = "defaults::server_host")]
    host: String,
    #[serde(default = "defaults::server_port")]
    port: u16,
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self { host: defaults::server_host(), port: defaults::server_port() }
    }
}

/// Effective daemon configuration, resolved from NCL → env → CLI defaults.
#[derive(Debug, serde::Deserialize)]
struct DaemonSettings {
    #[serde(default)]
    server: ServerConfig,
    #[serde(default = "defaults::orchestrator_url")]
    orchestrator_url: String,
    #[serde(default = "defaults::nats_url")]
    nats_url: String,
    #[serde(default = "defaults::project_name")]
    project_name: String,
    #[serde(default = "defaults::provisioning_bin")]
    provisioning_bin: String,
    #[serde(default)]
    watch_paths: Vec<std::path::PathBuf>,
    nickel_import_path: Option<String>,
    project_root: Option<std::path::PathBuf>,
    workspaces_root: Option<std::path::PathBuf>,
    extensions_root: Option<std::path::PathBuf>,
    ontology_templates: Option<std::path::PathBuf>,
    ui_templates_dir: Option<std::path::PathBuf>,
    control_center_url: Option<String>,
}

impl DaemonSettings {
    fn bind_addr(&self) -> String {
        format!("{}:{}", self.server.host, self.server.port)
    }
}

mod defaults {
    pub fn server_host() -> String { "0.0.0.0".into() }
    pub fn server_port() -> u16 { 9014 }
    pub fn orchestrator_url() -> String { "http://localhost:9011".into() }
    pub fn nats_url() -> String { "nats://127.0.0.1:4222".into() }
    pub fn project_name() -> String { "provisioning".into() }
    pub fn provisioning_bin() -> String { "provisioning".into() }
}

impl Default for DaemonSettings {
    fn default() -> Self {
        Self {
            server: ServerConfig::default(),
            orchestrator_url: defaults::orchestrator_url(),
            nats_url: defaults::nats_url(),
            project_name: defaults::project_name(),
            provisioning_bin: defaults::provisioning_bin(),
            watch_paths: vec![],
            nickel_import_path: None,
            project_root: None,
            workspaces_root: None,
            extensions_root: None,
            ontology_templates: None,
            ui_templates_dir: None,
            control_center_url: None,
        }
    }
}

/// Builds the NCL overrides JSON from environment variables.
///
/// Only env vars that are explicitly set override NCL defaults. Clap's
/// built-in default_value fields are intentionally excluded so NCL values
/// win over built-in defaults.
fn build_ncl_overrides() -> serde_json::Value {
    let mut daemon = serde_json::Map::new();

    let env_string = |key: &str| std::env::var(key).ok().map(serde_json::Value::String);

    // PROVISIONING_DAEMON_BIND="host:port" → server.host + server.port
    if let Ok(bind) = std::env::var("PROVISIONING_DAEMON_BIND") {
        if let Some((host, port_str)) = bind.rsplit_once(':') {
            if let Ok(port) = port_str.parse::<u16>() {
                daemon.insert(
                    "server".into(),
                    serde_json::json!({"host": host, "port": port}),
                );
            }
        }
    }

    for (env_key, ncl_key) in [
        ("ORCHESTRATOR_URL", "orchestrator_url"),
        ("NATS_URL", "nats_url"),
        ("PROVISIONING_PROJECT_NAME", "project_name"),
        ("PROVISIONING_BIN", "provisioning_bin"),
        // NICKEL_IMPORT_PATH is intentionally excluded: it is additive (prepended)
        // and already resolved via cli.nickel_import_path in resolve(). Injecting it
        // as a priority-1 NCL override would replace the config's nickel_import_path
        // (which may list ontoref or other paths) instead of prepending to it.
        ("PROVISIONING_CONTROL_CENTER_URL", "control_center_url"),
        ("PROVISIONING_PROJECT_ROOT", "project_root"),
        ("PROVISIONING_WORKSPACES_ROOT", "workspaces_root"),
        ("PROVISIONING_EXTENSIONS_ROOT", "extensions_root"),
        ("PROVISIONING_ONTOLOGY_TEMPLATES", "ontology_templates"),
        ("PROVISIONING_UI_TEMPLATES_DIR", "ui_templates_dir"),
    ] {
        if let Some(v) = env_string(env_key) {
            daemon.insert(ncl_key.into(), v);
        }
    }

    if daemon.is_empty() {
        serde_json::Value::Null
    } else {
        serde_json::json!({ "provisioning_daemon": serde_json::Value::Object(daemon) })
    }
}

/// Loads `DaemonSettings` from the NCL config hierarchy.
///
/// Priority:
/// 1. `-c/--config` explicit file path
/// 2. `--config-dir` directory with mode-aware file search
/// 3. Standard platform location: `~/Library/Application Support/provisioning/platform/config/provisioning-daemon.ncl`
/// 4. Built-in defaults (when NCL is unavailable or not configured)
fn load_daemon_settings(cli: &Cli) -> DaemonSettings {
    let overrides = build_ncl_overrides();

    let json_result: Result<serde_json::Value, _> = if let Some(ref path) = cli.config {
        platform_config::format::load_config_with_overrides(path, &overrides)
            .map_err(|e| anyhow::anyhow!("{e}"))
    } else if let Some(ref dir) = cli.config_dir {
        let mode = cli.mode.as_deref().unwrap_or("solo");
        // Honour both `config_dir/provisioning-daemon.ncl` and the platform
        // convention of keeping service configs one level deeper in `config/`.
        let config_sub = dir.join("config");
        let base = if config_sub.is_dir() { config_sub } else { dir.clone() };
        let mode_path = base.join(format!("provisioning-daemon.{mode}.ncl"));
        let plain_path = base.join("provisioning-daemon.ncl");
        let search = if mode_path.exists() { mode_path } else { plain_path };
        platform_config::format::load_config_with_overrides(search, &overrides)
            .map_err(|e| anyhow::anyhow!("{e}"))
    } else {
        load_service_config_from_ncl_with_overrides("provisioning-daemon", &overrides)
            .map_err(|e| anyhow::anyhow!("{e}"))
    };

    match json_result {
        Ok(json) => {
            let section = json
                .get("provisioning_daemon")
                .cloned()
                .unwrap_or(json);
            serde_json::from_value(section).unwrap_or_else(|e| {
                tracing::warn!("daemon config schema mismatch ({e}), using defaults");
                DaemonSettings::default()
            })
        }
        Err(e) => {
            tracing::debug!("NCL config not loaded ({e}), using CLI/env defaults");
            DaemonSettings::default()
        }
    }
}

/// Merges `DaemonSettings` (from NCL) with explicit CLI Option fields.
///
/// NCL values are the authoritative defaults. CLI `Option<T>` fields override
/// when the user explicitly provided them. String-with-defaults fields in the
/// Cli struct are intentionally shadowed by NCL when the user hasn't set the
/// corresponding env var.
fn resolve(ncl: DaemonSettings, cli: &Cli) -> DaemonSettings {
    DaemonSettings {
        server: ncl.server,
        orchestrator_url: ncl.orchestrator_url,
        nats_url: ncl.nats_url,
        project_name: ncl.project_name,
        provisioning_bin: ncl.provisioning_bin,
        watch_paths: if !cli.watch_paths.is_empty() {
            cli.watch_paths.clone()
        } else {
            ncl.watch_paths
        },
        // NICKEL_IMPORT_PATH env/CLI is additive: prepend to the NCL-configured path
        // so both the project repo and any user-specified paths (ontoref, etc.) are included.
        nickel_import_path: match (cli.nickel_import_path.clone(), ncl.nickel_import_path) {
            (Some(cli_ip), Some(ncl_ip)) => Some(format!("{cli_ip}:{ncl_ip}")),
            (Some(cli_ip), None) => Some(cli_ip),
            (None, ncl_ip) => ncl_ip,
        },
        project_root: cli.project_root.clone().or(ncl.project_root),
        workspaces_root: cli.workspaces_root.clone().or(ncl.workspaces_root),
        extensions_root: cli.extensions_root.clone().or(ncl.extensions_root),
        ontology_templates: cli.ontology_templates.clone().or(ncl.ontology_templates),
        ui_templates_dir: cli.ui_templates_dir.clone().or(ncl.ui_templates_dir),
        control_center_url: cli.control_center_url.clone().or(ncl.control_center_url),
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();

    // Load NCL config first (best-effort), then merge CLI Option fields on top.
    let cfg = resolve(load_daemon_settings(&cli), &cli);

    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::from_default_env()
                .add_directive(tracing::Level::INFO.into()),
        )
        .with_writer(std::io::stderr)
        .init();

    tracing::info!(
        bind = %cfg.bind_addr(),
        orchestrator_url = %cfg.orchestrator_url,
        nats_url = %cfg.nats_url,
        "provisioning-daemon config resolved"
    );

    // Build the effective NICKEL_IMPORT_PATH:
    //   project_root (for schemas/ + extensions/) + nickel_import_path (for ontoref etc.)
    // Deduplicate entries to avoid Nickel complaining about repeated paths.
    let effective_import_path: Option<String> = {
        let mut parts: Vec<String> = Vec::new();
        if let Some(ref root) = cfg.project_root {
            parts.push(root.display().to_string());
        }
        let extra_segments = cfg.nickel_import_path.as_deref().unwrap_or("").split(':');
        for segment in extra_segments {
            let s = segment.to_string();
            if !s.is_empty() && !parts.contains(&s) {
                parts.push(s);
            }
        }
        if parts.is_empty() { None } else { Some(parts.join(":")) }
    };

    // Propagate the resolved path back into the process environment so that
    // internal nickel::export_to_json calls (e.g. ontology context exports) use
    // the same combined path instead of whatever was inherited from the shell.
    if let Some(ref ip) = effective_import_path {
        std::env::set_var("NICKEL_IMPORT_PATH", ip);
    }

    // Registry
    let cache = Arc::new(match effective_import_path.as_deref() {
        Some(ip) => NclCache::with_import_path(ip),
        None     => NclCache::new(),
    });
    tracing::info!(
        import_path = effective_import_path.as_deref().unwrap_or("(none)"),
        "nickel import path"
    );
    let orch   = Arc::new(OrchestratorClient::new(&cfg.orchestrator_url));
    let hcloud = Arc::new(HcloudClient::new());
    let registry = Registry::with_all_tools(cache, orch, hcloud, None)
        .context("registry init")?;

    // Environment
    let mut env = Environment {
        orchestrator_url: cfg.orchestrator_url.clone(),
        ..Default::default()
    };

    // Derive workspaces_root and extensions_root from project_root when not explicit.
    if let Some(ref ws) = cfg.workspaces_root {
        env.workspaces_root = ws.clone();
    } else if let Some(ref root) = cfg.project_root {
        if let Some(parent) = root.parent() {
            let candidate = parent.join("workspaces");
            if candidate.exists() {
                env.workspaces_root = candidate;
            }
        }
    }

    if let Some(ref ext) = cfg.extensions_root {
        env.extensions_root = ext.clone();
    } else if let Some(ref root) = cfg.project_root {
        let candidate = root.join("extensions");
        if candidate.exists() {
            env.extensions_root = candidate;
        }
    }

    if !env.workspaces_root.as_os_str().is_empty() {
        tracing::info!(path = %env.workspaces_root.display(), "workspaces root");
    } else {
        tracing::warn!("workspaces_root not set — workspace_list tool will return empty");
    }
    if !env.extensions_root.as_os_str().is_empty() {
        tracing::info!(path = %env.extensions_root.display(), "extensions root");
    }

    // Capture for startup banner before env is moved into AppState.
    let banner_ws = if env.workspaces_root.as_os_str().is_empty() {
        "(not set)".to_string()
    } else {
        env.workspaces_root.display().to_string()
    };
    let banner_ext = if env.extensions_root.as_os_str().is_empty() {
        "(not set)".to_string()
    } else {
        env.extensions_root.display().to_string()
    };

    // Session persistence path: XDG_DATA_HOME/provisioning/daemon-sessions.json
    let session_file = {
        let base = std::env::var("XDG_DATA_HOME")
            .unwrap_or_else(|_| format!("{}/.local/share", env!("HOME")));
        std::path::PathBuf::from(base)
            .join("provisioning")
            .join("daemon-sessions.json")
    };

    // App state
    let mut app = AppState::new(registry, env)
        .with_project_name(cfg.project_name.clone())
        .with_provisioning_bin(cfg.provisioning_bin.as_str())
        .with_session_persistence(session_file);

    if let Some(ref path) = effective_import_path {
        app = app.with_nickel_import_path(path.as_str());
    }

    // Project root (enables ontology graph + modes)
    if let Some(ref root) = cfg.project_root {
        let canonical = root.canonicalize()
            .with_context(|| format!("project-root {}: canonicalize failed", root.display()))?;
        app = app.with_project_root(canonical);
        tracing::info!(root = %root.display(), "project root set");
    }

    // SSR admin UI
    let ui = match &cfg.ui_templates_dir {
        Some(dir) => {
            match UiRenderer::from_dir(dir) {
                Ok(r) => { tracing::info!(dir = %dir.display(), "ui templates loaded from disk"); r }
                Err(e) => {
                    tracing::warn!("ui templates from disk failed ({e}), falling back to embedded");
                    UiRenderer::from_embedded().context("embedded ui templates")?
                }
            }
        }
        None => UiRenderer::from_embedded().context("embedded ui templates")?,
    };
    app = app.with_ui(ui);
    tracing::info!("ui ready at /ui/");

    // Optional ontology document templates
    if let Some(ref dir) = cfg.ontology_templates {
        match OntologyRenderer::from_dir(dir) {
            Ok(renderer) => {
                let n = renderer.template_names().len();
                app = app.with_ontology(renderer);
                tracing::info!(count = n, dir = %dir.display(), "ontology templates loaded");
            }
            Err(e) => tracing::warn!("ontology templates failed to load from {}: {e}", dir.display()),
        }
    }

    // Optional JWT (CLI-only — not in NCL, sensitive)
    if let Some(ref secret) = cli.jwt_secret {
        let jwt = JwtConfig::from_secret(secret.as_bytes())
            .context("PROVISIONING_JWT_SECRET rejected (must be >= 32 bytes for HS256)")?;
        app = app.with_jwt(jwt);
        tracing::info!("JWT auth enabled");
    } else {
        tracing::warn!("JWT disabled — running in open/solo mode");
    }

    // UI auth (CLI-only — sensitive)
    if let Some(ref pw) = cli.ui_password {
        app = app.with_ui_password(pw.as_str());
        tracing::info!("UI password auth configured");
    }
    if let Some(ref url) = cfg.control_center_url {
        app = app.with_control_center_url(url.as_str());
        tracing::info!(url = %url, "control-center auth delegation configured");
    }

    // Optional NATS event bus
    let nats_cfg = platform_nats::NatsConfig {
        url: cfg.nats_url.clone(),
        ..Default::default()
    };
    match platform_nats::NatsBridge::connect(&nats_cfg).await {
        Ok(bridge) => {
            app = app.with_bus(EventBus::new(bridge));
            tracing::info!(url = %cfg.nats_url, "NATS connected");
        }
        Err(e) => {
            tracing::warn!("NATS unavailable ({e}) — events disabled");
        }
    }

    // Resolve bind address before watch_paths is moved
    let bind_addr_str = cfg.bind_addr();

    // Config file watcher
    let watch_paths_display = cfg.watch_paths.iter()
        .map(|p| p.display().to_string())
        .collect::<Vec<_>>();
    if !cfg.watch_paths.is_empty() {
        match provisioning_daemon::watcher::spawn_watcher(cfg.watch_paths) {
            Ok((rx, _watcher)) => {
                let state = app.state.clone();
                let bus   = app.bus.clone();
                tokio::spawn(provisioning_daemon::watcher::run_config_watch_loop(rx, state, bus));
                tracing::info!("config watcher started");
            }
            Err(e) => tracing::warn!("config watcher failed to start: {e}"),
        }
    }

    // HTTP server
    let addr: std::net::SocketAddr = bind_addr_str.parse().context("invalid bind address")?;
    let listener = TcpListener::bind(addr).await?;

    // Emit a single resolved-config banner so operators can verify state at a glance.
    {
        let ws = &banner_ws;
        let ext = &banner_ext;
        let root_display = cfg.project_root
            .as_ref()
            .map(|p| p.display().to_string())
            .unwrap_or_else(|| "(not set)".to_string());
        let nickel_ip = effective_import_path.as_deref().unwrap_or("(none)");
        let auth_mode = if cli.jwt_secret.is_some() { "jwt" } else { "solo/open" };
        let ui_src = cfg.ui_templates_dir
            .as_ref()
            .map(|p| format!("disk:{}", p.display()))
            .unwrap_or_else(|| "embedded".to_string());

        eprintln!();
        let watch_display = if watch_paths_display.is_empty() {
            "(none)".to_string()
        } else {
            watch_paths_display.join(", ")
        };
        eprintln!("-- provisioning-daemon ready --------------------------------");
        eprintln!("   url              http://{bind_addr_str}");
        eprintln!("   project_root     {root_display}");
        eprintln!("   workspaces_root  {ws}");
        eprintln!("   extensions_root  {ext}");
        eprintln!("   orchestrator     {}", cfg.orchestrator_url);
        eprintln!("   nats             {}", cfg.nats_url);
        eprintln!("   nickel_path      {nickel_ip}");
        eprintln!("   ui               {ui_src}");
        eprintln!("   auth             {auth_mode}");
        eprintln!("   watch_paths      {watch_display}");
        eprintln!("------------------------------------------------------------");
        eprintln!();
    }

    tracing::info!("provisioning-daemon listening on http://{addr}");

    let app_router = provisioning_daemon::ui::ui_router(&app)
        .route("/", axum::routing::get(|| async { axum::response::Redirect::permanent("/ui/") }))
        .merge(provisioning_daemon::http::raw_router(&app))
        .with_state(app);

    axum::serve(listener, app_router).await?;
    Ok(())
}
