//! First-run admin seeding from env vars. Idempotent: if any admin
//! already exists, this is a no-op (and a warning is logged when the
//! env vars are also set, so the operator notices the inert state).
//!
//! On a fresh install, exactly one row is inserted from:
//!   - `PICLOUD_ADMIN_USERNAME` (required)
//!   - `PICLOUD_ADMIN_PASSWORD_HASH` (preferred — pre-computed PHC) OR
//!   - `PICLOUD_ADMIN_PASSWORD` (fallback — raw, hashed on the way in)
//!
//! After that initial seed, the env vars become inert. This is
//! deliberate: the env var is a one-time setup hatch, not a permanent
//! override (which would let anyone with systemd/compose access change
//! any admin's password without authentication). Recovery is the CLI
//! subcommand `picloud admin reset-password <username>`.
//!
//! The env-var reading is factored into `BootstrapEnv::from_process`
//! so the core logic stays pure (and testable) — the only side effect
//! in `bootstrap_first_admin` is the DB write and a tracing log.

use tracing::{info, warn};

use crate::admin_user_repo::AdminUserRepository;
use crate::auth::{hash_password, validate_password_hash};

pub const ENV_USERNAME: &str = "PICLOUD_ADMIN_USERNAME";
pub const ENV_PASSWORD: &str = "PICLOUD_ADMIN_PASSWORD";
pub const ENV_PASSWORD_HASH: &str = "PICLOUD_ADMIN_PASSWORD_HASH";

#[derive(Debug, thiserror::Error)]
pub enum BootstrapError {
    #[error("repository error: {0}")]
    Repo(#[from] crate::admin_user_repo::AdminUserRepositoryError),

    #[error("{ENV_USERNAME} not set (required to bootstrap the first admin)")]
    MissingUsername,

    #[error(
        "no admin password env var set; provide {ENV_PASSWORD_HASH} (preferred) or {ENV_PASSWORD}"
    )]
    MissingPassword,

    #[error("{ENV_PASSWORD_HASH} is not a valid Argon2id PHC string")]
    InvalidHash,

    #[error("failed to hash password: {0}")]
    HashFailure(String),
}

/// Captured-at-call-site env values. The fields map 1:1 to the bootstrap
/// env vars. Read from the live process with `from_process`, or build
/// directly in tests to keep them free of process-env races.
#[derive(Debug, Default, Clone)]
pub struct BootstrapEnv {
    pub username: Option<String>,
    pub password: Option<String>,
    pub password_hash: Option<String>,
}

impl BootstrapEnv {
    /// Snapshot the bootstrap env vars from the current process.
    #[must_use]
    pub fn from_process() -> Self {
        Self {
            username: std::env::var(ENV_USERNAME).ok(),
            password: std::env::var(ENV_PASSWORD).ok(),
            password_hash: std::env::var(ENV_PASSWORD_HASH).ok(),
        }
    }

    fn any_set(&self) -> bool {
        self.username.is_some() || self.password.is_some() || self.password_hash.is_some()
    }
}

/// Run the bootstrap. Reads env vars from the live process — the
/// canonical wiring for the binary.
pub async fn bootstrap_first_admin<R: AdminUserRepository + ?Sized>(
    repo: &R,
) -> Result<(), BootstrapError> {
    bootstrap_first_admin_with(repo, BootstrapEnv::from_process()).await
}

/// Run the bootstrap against an explicit env. Used by tests to keep
/// the bootstrap logic independent of process state.
pub async fn bootstrap_first_admin_with<R: AdminUserRepository + ?Sized>(
    repo: &R,
    env: BootstrapEnv,
) -> Result<(), BootstrapError> {
    if repo.count_active().await? > 0 {
        if env.any_set() {
            warn!(
                "{ENV_USERNAME}/{ENV_PASSWORD}/{ENV_PASSWORD_HASH} set but admin_users \
                 already populated — env values ignored. Use \
                 `picloud admin reset-password <user>` to change a password."
            );
        }
        return Ok(());
    }

    let username = env.username.ok_or(BootstrapError::MissingUsername)?;

    let password_hash = match (env.password_hash, env.password) {
        (Some(hash), maybe_raw) => {
            if maybe_raw.is_some() {
                warn!(
                    "both {ENV_PASSWORD_HASH} and {ENV_PASSWORD} set — \
                     using the pre-computed hash; raw password ignored."
                );
            }
            validate_password_hash(&hash).map_err(|_| BootstrapError::InvalidHash)?;
            hash
        }
        (None, Some(raw)) => {
            hash_password(&raw).map_err(|e| BootstrapError::HashFailure(e.to_string()))?
        }
        (None, None) => return Err(BootstrapError::MissingPassword),
    };

    // Bootstrap admin is always seeded as Owner — Phase 3.5 keys the
    // first row to full instance control. Subsequent admins minted via
    // the API default to Admin and can be promoted explicitly.
    repo.create(
        &username,
        &password_hash,
        picloud_shared::InstanceRole::Owner,
        None,
    )
    .await?;
    info!(username = %username, "bootstrapped initial admin user");
    Ok(())
}

#[cfg(test)]
mod tests {
    //! These tests use an in-memory `AdminUserRepository` and the
    //! `bootstrap_first_admin_with` overload so they never touch
    //! process-global env vars. They can run in parallel safely.

    use super::*;
    use async_trait::async_trait;
    use chrono::Utc;
    use picloud_shared::{AdminUserId, InstanceRole};
    use std::sync::Mutex;

    use crate::admin_user_repo::{AdminUserCredentials, AdminUserRepositoryError, AdminUserRow};

    #[derive(Default)]
    struct InMemoryRepo {
        rows: Mutex<Vec<AdminUserRow>>,
    }

    #[async_trait]
    impl AdminUserRepository for InMemoryRepo {
        async fn get(
            &self,
            _id: AdminUserId,
        ) -> Result<Option<AdminUserRow>, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn get_by_username(
            &self,
            _u: &str,
        ) -> Result<Option<AdminUserRow>, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn get_credentials_by_username(
            &self,
            _u: &str,
        ) -> Result<Option<AdminUserCredentials>, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn list(&self) -> Result<Vec<AdminUserRow>, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn create(
            &self,
            username: &str,
            _password_hash: &str,
            instance_role: InstanceRole,
            email: Option<&str>,
        ) -> Result<AdminUserRow, AdminUserRepositoryError> {
            let row = AdminUserRow {
                id: AdminUserId::new(),
                username: username.to_string(),
                is_active: true,
                instance_role,
                email: email.map(str::to_string),
                created_at: Utc::now(),
                updated_at: Utc::now(),
                last_login_at: None,
            };
            self.rows.lock().unwrap().push(row.clone());
            Ok(row)
        }
        async fn update_username(
            &self,
            _i: AdminUserId,
            _u: &str,
        ) -> Result<AdminUserRow, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn update_password_hash(
            &self,
            _i: AdminUserId,
            _h: &str,
        ) -> Result<AdminUserRow, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn update_email(
            &self,
            _i: AdminUserId,
            _e: Option<&str>,
        ) -> Result<AdminUserRow, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn update_instance_role(
            &self,
            _i: AdminUserId,
            _r: InstanceRole,
        ) -> Result<AdminUserRow, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn set_active(
            &self,
            _i: AdminUserId,
            _a: bool,
        ) -> Result<AdminUserRow, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn delete(&self, _i: AdminUserId) -> Result<(), AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn touch_last_login(&self, _i: AdminUserId) -> Result<(), AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn count_active(&self) -> Result<i64, AdminUserRepositoryError> {
            Ok(i64::try_from(self.rows.lock().unwrap().len()).unwrap_or(i64::MAX))
        }
        async fn count_active_excluding(
            &self,
            _i: AdminUserId,
        ) -> Result<i64, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn list_active_owners(&self) -> Result<Vec<AdminUserRow>, AdminUserRepositoryError> {
            unimplemented!()
        }
        async fn count_other_active_owners(
            &self,
            _i: AdminUserId,
        ) -> Result<i64, AdminUserRepositoryError> {
            unimplemented!()
        }
    }

    #[tokio::test]
    async fn empty_db_creates_admin_from_raw_password() {
        let repo = InMemoryRepo::default();
        let env = BootstrapEnv {
            username: Some("alice".into()),
            password: Some("supersecret".into()),
            password_hash: None,
        };
        bootstrap_first_admin_with(&repo, env).await.unwrap();
        assert_eq!(repo.rows.lock().unwrap().len(), 1);
    }

    #[tokio::test]
    async fn empty_db_with_pre_hashed_password_succeeds() {
        let repo = InMemoryRepo::default();
        let prehashed = hash_password("pw").unwrap();
        let env = BootstrapEnv {
            username: Some("alice".into()),
            password: None,
            password_hash: Some(prehashed),
        };
        bootstrap_first_admin_with(&repo, env).await.unwrap();
        assert_eq!(repo.rows.lock().unwrap().len(), 1);
    }

    #[tokio::test]
    async fn populated_db_is_noop() {
        let repo = InMemoryRepo::default();
        repo.create("seeded", "x", InstanceRole::Owner, None)
            .await
            .unwrap();
        let env = BootstrapEnv {
            username: Some("alice".into()),
            password: Some("supersecret".into()),
            password_hash: None,
        };
        bootstrap_first_admin_with(&repo, env).await.unwrap();
        assert_eq!(repo.rows.lock().unwrap().len(), 1);
    }

    #[tokio::test]
    async fn missing_username_fails() {
        let repo = InMemoryRepo::default();
        let env = BootstrapEnv {
            username: None,
            password: Some("supersecret".into()),
            password_hash: None,
        };
        let err = bootstrap_first_admin_with(&repo, env).await.unwrap_err();
        assert!(matches!(err, BootstrapError::MissingUsername));
    }

    #[tokio::test]
    async fn missing_password_fails() {
        let repo = InMemoryRepo::default();
        let env = BootstrapEnv {
            username: Some("alice".into()),
            password: None,
            password_hash: None,
        };
        let err = bootstrap_first_admin_with(&repo, env).await.unwrap_err();
        assert!(matches!(err, BootstrapError::MissingPassword));
    }

    #[tokio::test]
    async fn invalid_hash_fails() {
        let repo = InMemoryRepo::default();
        let env = BootstrapEnv {
            username: Some("alice".into()),
            password: None,
            password_hash: Some("not a phc hash".into()),
        };
        let err = bootstrap_first_admin_with(&repo, env).await.unwrap_err();
        assert!(matches!(err, BootstrapError::InvalidHash));
    }
}
