//! PiCloud all-in-one binary — see `lib.rs` for the actual app
//! composition; this file is only the runtime shell (env config,
//! logger, migrations, listener) plus the small `admin` CLI subcommand
//! used for out-of-band password recovery.

use std::io::{BufRead, Write};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use picloud::{build_app, init_db, AuthDeps};
use picloud_manager_core::{
    auth::{hash_password, validate_password_hash},
    bootstrap_first_admin, migrations, seed_hello_world_if_fresh, AdminSessionRepository,
    AdminUserRepository, HelloWorldOutcome, PostgresAppRepository, PostgresRouteRepository,
    PostgresScriptRepository,
};
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    init_tracing();

    // Subcommand dispatch — `picloud admin reset-password <username>`.
    // Kept handwritten to avoid pulling clap in just for one verb. Falls
    // through to the server when no subcommand is given.
    let args: Vec<String> = std::env::args().collect();
    if args.get(1).map(String::as_str) == Some("admin") {
        return run_admin_cli(&args[2..]).await;
    }

    run_server().await
}

async fn run_server() -> anyhow::Result<()> {
    let addr: SocketAddr = std::env::var("PICLOUD_BIND")
        .unwrap_or_else(|_| "0.0.0.0:8080".into())
        .parse()?;
    let database_url =
        std::env::var("DATABASE_URL").map_err(|_| anyhow::anyhow!("DATABASE_URL is required"))?;

    // Source the process master key BEFORE doing any work — an unset or
    // malformed PICLOUD_SECRET_KEY is fatal (v1.1.7). The only escape
    // hatch is PICLOUD_DEV_MODE=true, which logs a prominent warning.
    let master_key = picloud_shared::MasterKey::from_env()?;

    let pool = init_db(&database_url).await?;
    migrations::run(&pool).await?;
    tracing::info!("migrations applied");

    let auth = AuthDeps::from_pool(pool.clone());
    bootstrap_first_admin(&*auth.users).await?;
    warn_on_multi_owner_install(&*auth.users).await;

    // Seed Hello World into the default app when this is a fresh
    // install (no scripts and no routes). Idempotent on upgrades.
    let apps = Arc::new(PostgresAppRepository::new(pool.clone()));
    let scripts = Arc::new(PostgresScriptRepository::new(pool.clone()));
    let routes = Arc::new(PostgresRouteRepository::new(pool.clone()));
    match seed_hello_world_if_fresh(apps, scripts, routes).await {
        Ok(HelloWorldOutcome::Seeded) => {
            tracing::info!("hello-world seed inserted into the default app");
        }
        Ok(HelloWorldOutcome::SkippedExisting) => {
            tracing::debug!("hello-world seed skipped (default app already populated)");
        }
        Err(err) => {
            tracing::warn!(?err, "hello-world seed failed (continuing startup)");
        }
    }

    // Background session-prune sweep. Cheap; keeps the table from
    // growing unbounded. Expired rows are also rejected at lookup time,
    // so a delayed sweep can't extend session lifetimes.
    spawn_session_pruner(auth.sessions.clone());

    let app = build_app(pool, auth, master_key).await?;

    let listener = tokio::net::TcpListener::bind(addr).await?;
    tracing::info!(%addr, "picloud all-in-one listening");
    axum::serve(listener, app)
        .with_graceful_shutdown(shutdown_signal())
        .await?;

    Ok(())
}

/// Multi-owner startup warning — Phase 3.5 migration upgraded every
/// pre-existing admin_users row to `Owner` via DEFAULT, which for
/// installs with several Phase 3a admins means several co-owners.
/// Surface this once at boot so the operator can demote extras via
/// `PATCH /api/v1/admin/admins/{id}` with `instance_role: "admin"`.
/// Soft-fail: a DB blip should not block startup.
async fn warn_on_multi_owner_install(users: &dyn AdminUserRepository) {
    match users.list_active_owners().await {
        Ok(owners) if owners.len() > 1 => {
            let names: Vec<String> = owners.into_iter().map(|u| u.username).collect();
            tracing::warn!(
                count = names.len(),
                owners = ?names,
                "multiple active owners detected — Phase 3.5 promoted every \
                 pre-existing admin to owner. Demote extras via \
                 PATCH /api/v1/admin/admins/{{id}} with instance_role."
            );
        }
        Ok(_) => {}
        Err(err) => {
            tracing::warn!(
                ?err,
                "could not count active owners for multi-owner startup check"
            );
        }
    }
}

fn spawn_session_pruner(sessions: Arc<dyn AdminSessionRepository>) {
    tokio::spawn(async move {
        let mut ticker = tokio::time::interval(Duration::from_secs(600));
        // First tick fires immediately; skip it so we don't race startup.
        ticker.tick().await;
        loop {
            ticker.tick().await;
            match sessions.prune_expired().await {
                Ok(n) if n > 0 => tracing::debug!(pruned = n, "expired admin sessions pruned"),
                Ok(_) => {}
                Err(err) => tracing::warn!(?err, "admin session prune failed"),
            }
        }
    });
}

// ----------------------------------------------------------------------------
// `admin` subcommand
// ----------------------------------------------------------------------------

async fn run_admin_cli(args: &[String]) -> anyhow::Result<()> {
    match args.first().map(String::as_str) {
        Some("reset-password") => {
            let username = args.get(1).ok_or_else(|| {
                anyhow::anyhow!(
                    "usage: picloud admin reset-password <username> [--password-hash <hash>]"
                )
            })?;
            // Optional inline hash via --password-hash <hash>; otherwise
            // read a raw password from stdin.
            let hash_arg = parse_flag(&args[2..], "--password-hash");
            cmd_reset_password(username, hash_arg).await
        }
        Some(other) => Err(anyhow::anyhow!("unknown admin subcommand: {other}")),
        None => Err(anyhow::anyhow!(
            "usage: picloud admin reset-password <username>"
        )),
    }
}

fn parse_flag(args: &[String], name: &str) -> Option<String> {
    let mut it = args.iter();
    while let Some(a) = it.next() {
        if a == name {
            return it.next().cloned();
        }
    }
    None
}

async fn cmd_reset_password(username: &str, password_hash: Option<String>) -> anyhow::Result<()> {
    let database_url =
        std::env::var("DATABASE_URL").map_err(|_| anyhow::anyhow!("DATABASE_URL is required"))?;
    let pool = init_db(&database_url).await?;
    migrations::run(&pool).await?;

    let users = picloud_manager_core::PostgresAdminUserRepository::new(pool.clone());
    let sessions = picloud_manager_core::PostgresAdminSessionRepository::new(pool);

    let target = users
        .get_by_username(username)
        .await?
        .ok_or_else(|| anyhow::anyhow!("no admin user named {username:?}"))?;

    let hash = if let Some(h) = password_hash {
        validate_password_hash(&h)
            .map_err(|_| anyhow::anyhow!("--password-hash is not a valid Argon2id PHC string"))?;
        h
    } else {
        let raw = prompt_password_from_stdin()?;
        hash_password(&raw).map_err(|e| anyhow::anyhow!("failed to hash password: {e}"))?
    };

    users.update_password_hash(target.id, &hash).await?;
    // Recovery implies the operator already lost control of the account;
    // re-activate it (so a deactivated admin can also recover) and wipe
    // any pre-existing sessions in case the original holder is still
    // signed in elsewhere.
    if !target.is_active {
        users.set_active(target.id, true).await?;
    }
    let dropped = sessions.delete_for_user(target.id).await?;

    println!("Password reset for {username}. Sessions dropped: {dropped}. Active: true.");
    Ok(())
}

fn prompt_password_from_stdin() -> anyhow::Result<String> {
    eprint!("New password (will be read from stdin, no echo): ");
    std::io::stderr().flush().ok();
    let mut line = String::new();
    std::io::stdin()
        .lock()
        .read_line(&mut line)
        .map_err(|e| anyhow::anyhow!("failed to read stdin: {e}"))?;
    let pw = line.trim_end_matches(['\n', '\r']).to_string();
    if pw.is_empty() {
        return Err(anyhow::anyhow!("password must not be empty"));
    }
    Ok(pw)
}

// ----------------------------------------------------------------------------
// Misc
// ----------------------------------------------------------------------------

fn init_tracing() {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
        .json()
        .init();
}

async fn shutdown_signal() {
    let ctrl_c = async {
        let _ = tokio::signal::ctrl_c().await;
    };
    #[cfg(unix)]
    let terminate = async {
        if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
        {
            s.recv().await;
        }
    };
    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        () = ctrl_c => tracing::info!("ctrl-c received, draining"),
        () = terminate => tracing::info!("SIGTERM received, draining"),
    }
}
