// Authentication module for Archipelago
// Handles user setup, onboarding, and login

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::fs;

use crate::totp::TotpData;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct OnboardingState {
    complete: bool,
}

#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub password_hash: String,
    pub setup_complete: bool,
    pub onboarding_complete: bool,
    #[serde(default)]
    pub totp: Option<TotpData>,
}

#[allow(dead_code)]
pub struct AuthManager {
    data_dir: PathBuf,
}

#[allow(dead_code)]
impl AuthManager {
    pub fn new(data_dir: PathBuf) -> Self {
        Self { data_dir }
    }

    pub async fn is_setup(&self) -> Result<bool> {
        let user_file = self.data_dir.join("user.json");
        Ok(user_file.exists())
    }

    pub async fn get_user(&self) -> Result<Option<User>> {
        let user_file = self.data_dir.join("user.json");
        if !user_file.exists() {
            return Ok(None);
        }

        let content = fs::read_to_string(&user_file).await?;
        let user: User = serde_json::from_str(&content)?;
        Ok(Some(user))
    }

    pub async fn setup_user(&self, password: &str) -> Result<()> {
        use bcrypt::{hash, DEFAULT_COST};

        let password_hash = hash(password, DEFAULT_COST)?;

        // If onboarding was already completed (before setup), preserve that
        let onboarding_complete = self.is_onboarding_complete().await?;

        let user = User {
            password_hash,
            setup_complete: true,
            onboarding_complete,
            totp: None,
        };

        let user_file = self.data_dir.join("user.json");
        let content = serde_json::to_string_pretty(&user)?;
        fs::write(&user_file, content).await?;

        Ok(())
    }

    pub async fn complete_onboarding(&self) -> Result<()> {
        // Persist to onboarding.json (works even before user/setup exists)
        let onboarding_file = self.data_dir.join("onboarding.json");
        let state = OnboardingState { complete: true };
        fs::write(
            &onboarding_file,
            serde_json::to_string_pretty(&state)?,
        )
        .await?;
        // Also update user.json if it exists (keeps them in sync)
        if let Some(mut user) = self.get_user().await? {
            user.onboarding_complete = true;
            let user_file = self.data_dir.join("user.json");
            let content = serde_json::to_string_pretty(&user)?;
            fs::write(&user_file, content).await?;
        }
        Ok(())
    }

    /// Reset onboarding state so the user can go through onboarding again (dev/testing).
    pub async fn reset_onboarding(&self) -> Result<()> {
        let onboarding_file = self.data_dir.join("onboarding.json");
        let state = OnboardingState { complete: false };
        fs::write(
            &onboarding_file,
            serde_json::to_string_pretty(&state)?,
        )
        .await?;
        if let Some(mut user) = self.get_user().await? {
            user.onboarding_complete = false;
            let user_file = self.data_dir.join("user.json");
            let content = serde_json::to_string_pretty(&user)?;
            fs::write(&user_file, content).await?;
        }
        Ok(())
    }

    pub async fn is_onboarding_complete(&self) -> Result<bool> {
        // Check onboarding.json first (persisted before user setup)
        let onboarding_file = self.data_dir.join("onboarding.json");
        if onboarding_file.exists() {
            let content = fs::read_to_string(&onboarding_file).await?;
            if let Ok(state) = serde_json::from_str::<OnboardingState>(&content) {
                if state.complete {
                    return Ok(true);
                }
            }
        }
        // Fallback: user.json
        Ok(self
            .get_user()
            .await?
            .map(|u| u.onboarding_complete)
            .unwrap_or(false))
    }

    /// Check if 2FA is enabled for the user.
    pub async fn is_totp_enabled(&self) -> Result<bool> {
        Ok(self.get_user().await?.map(|u| u.totp.is_some()).unwrap_or(false))
    }

    /// Get the TOTP data (if 2FA is enabled).
    pub async fn get_totp_data(&self) -> Result<Option<TotpData>> {
        Ok(self.get_user().await?.and_then(|u| u.totp))
    }

    /// Save TOTP data to user.json (enable 2FA).
    pub async fn save_totp(&self, totp_data: TotpData) -> Result<()> {
        let mut user = self.get_user().await?.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
        user.totp = Some(totp_data);
        let user_file = self.data_dir.join("user.json");
        fs::write(&user_file, serde_json::to_string_pretty(&user)?).await?;
        Ok(())
    }

    /// Remove TOTP data from user.json (disable 2FA).
    pub async fn remove_totp(&self) -> Result<()> {
        let mut user = self.get_user().await?.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
        user.totp = None;
        let user_file = self.data_dir.join("user.json");
        fs::write(&user_file, serde_json::to_string_pretty(&user)?).await?;
        Ok(())
    }

    /// Update TOTP data in place (e.g. after consuming a backup code or recording a used step).
    pub async fn update_totp(&self, totp_data: TotpData) -> Result<()> {
        self.save_totp(totp_data).await
    }

    pub async fn verify_password(&self, password: &str) -> Result<bool> {
        use bcrypt::verify;
        
        if let Some(user) = self.get_user().await? {
            Ok(verify(password, &user.password_hash)?)
        } else {
            Ok(false)
        }
    }

    /// Change password: verify current, validate new, update user.json and optionally SSH.
    /// New password must be 12+ chars with upper, lower, digit, and special character.
    pub async fn change_password(
        &self,
        current_password: &str,
        new_password: &str,
        also_change_ssh: bool,
    ) -> Result<()> {
        use bcrypt::{hash, DEFAULT_COST};

        if !self.verify_password(current_password).await? {
            anyhow::bail!("Current password is incorrect");
        }

        validate_password_strength(new_password)?;

        let password_hash = hash(new_password, DEFAULT_COST)?;

        let mut user = self
            .get_user()
            .await?
            .ok_or_else(|| anyhow::anyhow!("User not set up"))?;

        user.password_hash = password_hash;

        // Re-encrypt TOTP MEK under new password if 2FA is enabled
        if let Some(ref totp_data) = user.totp {
            let rekeyed = crate::totp::rekey(totp_data, current_password, new_password)?;
            user.totp = Some(rekeyed);
        }

        let user_file = self.data_dir.join("user.json");
        let content = serde_json::to_string_pretty(&user)?;
        fs::write(&user_file, content).await?;

        if also_change_ssh {
            change_ssh_password(new_password).await?;
        }

        Ok(())
    }
}

/// Validate password strength: 12+ chars, upper, lower, digit, special.
fn validate_password_strength(password: &str) -> Result<()> {
    if password.len() < 12 {
        anyhow::bail!("Password must be at least 12 characters");
    }
    if !password.chars().any(|c| c.is_ascii_uppercase()) {
        anyhow::bail!("Password must contain at least one uppercase letter");
    }
    if !password.chars().any(|c| c.is_ascii_lowercase()) {
        anyhow::bail!("Password must contain at least one lowercase letter");
    }
    if !password.chars().any(|c| c.is_ascii_digit()) {
        anyhow::bail!("Password must contain at least one digit");
    }
    if !password.chars().any(|c| !c.is_ascii_alphanumeric()) {
        anyhow::bail!("Password must contain at least one special character (!@#$%^&* etc.)");
    }
    Ok(())
}

/// Change the archipelago user's SSH/login password.
/// Uses usermod + openssl to bypass PAM (avoids "Authentication token manipulation" errors).
/// Uses absolute paths (/usr/bin/openssl, /usr/sbin/usermod) for systemd's minimal PATH.
async fn change_ssh_password(new_password: &str) -> Result<()> {
    let ssh_user = std::env::var("ARCHIPELAGO_SSH_USER").unwrap_or_else(|_| "archipelago".to_string());

    // Generate crypt hash via openssl (SHA-512, compatible with /etc/shadow)
    // Use /usr/bin/openssl - systemd services often have minimal PATH
    let mut hash_child = tokio::process::Command::new("/usr/bin/openssl")
        .args(["passwd", "-6", "-stdin"])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .map_err(|e| anyhow::anyhow!("Failed to run openssl: {}. Is openssl installed?", e))?;

    {
        use tokio::io::AsyncWriteExt;
        let mut stdin = hash_child
            .stdin
            .take()
            .ok_or_else(|| anyhow::anyhow!("Failed to open openssl stdin"))?;
        stdin.write_all(new_password.as_bytes()).await?;
        stdin.flush().await?;
    }

    let hash_result = hash_child.wait_with_output().await?;
    if !hash_result.status.success() {
        let stderr = String::from_utf8_lossy(&hash_result.stderr);
        anyhow::bail!("openssl passwd failed: {}", stderr);
    }
    let hash = String::from_utf8(hash_result.stdout)?
        .trim()
        .to_string();
    if hash.is_empty() {
        anyhow::bail!("openssl passwd produced empty hash");
    }

    // usermod -p writes directly to /etc/shadow, bypassing PAM
    // Use /usr/sbin/usermod - not always in systemd's PATH
    let status = tokio::process::Command::new("/usr/sbin/usermod")
        .args(["-p", &hash, &ssh_user])
        .output()
        .await?;

    if !status.status.success() {
        let stderr = String::from_utf8_lossy(&status.stderr);
        anyhow::bail!("usermod failed: {}", stderr);
    }

    tracing::info!("SSH password updated for user {}", ssh_user);
    Ok(())
}
