//! `relicario device {add, revoke, list}` — device key management.
//!
//! Note: command bodies live here as `crate::commands::device`. Local key
//! storage and git-signing config live separately in `crate::device`.

use anyhow::Result;

use crate::DeviceAction;

/// Build a `GiteaClient` from flags or environment variables.
fn load_gitea_client(
    gitea_url: Option<String>,
    gitea_token: Option<String>,
    owner: Option<String>,
    repo: Option<String>,
) -> Result<crate::gitea::GiteaClient> {
    let url = gitea_url
        .or_else(|| std::env::var("RELICARIO_GITEA_URL").ok())
        .ok_or_else(|| anyhow::anyhow!(
            "Gitea URL required — pass --gitea-url or set RELICARIO_GITEA_URL"
        ))?;
    let token = gitea_token
        .or_else(|| std::env::var("RELICARIO_GITEA_TOKEN").ok())
        .ok_or_else(|| anyhow::anyhow!(
            "Gitea token required — pass --gitea-token or set RELICARIO_GITEA_TOKEN"
        ))?;
    let owner = owner
        .or_else(|| std::env::var("RELICARIO_GITEA_OWNER").ok())
        .ok_or_else(|| anyhow::anyhow!(
            "Gitea owner required — pass --owner or set RELICARIO_GITEA_OWNER"
        ))?;
    let repo = repo
        .or_else(|| std::env::var("RELICARIO_GITEA_REPO").ok())
        .ok_or_else(|| anyhow::anyhow!(
            "Gitea repo required — pass --repo or set RELICARIO_GITEA_REPO"
        ))?;
    Ok(crate::gitea::GiteaClient::new(&url, &token, &owner, &repo))
}

pub fn cmd_device(action: DeviceAction) -> Result<()> {
    use std::fs;
    use relicario_core::device::{DeviceEntry, RevokedEntry, generate_keypair};

    let root = crate::helpers::vault_dir()?;
    let relicario_dir = root.join(".relicario");
    let devices_path = relicario_dir.join("devices.json");

    match action {
        DeviceAction::Add { name, gitea_url, gitea_token, owner, repo, no_gitea } => {
            // Guard: don't overwrite an already-registered device name.
            let existing: Vec<DeviceEntry> = fs::read(&devices_path)
                .ok()
                .and_then(|b| serde_json::from_slice(&b).ok())
                .unwrap_or_default();
            if existing.iter().any(|d| d.name == name) {
                anyhow::bail!("a device named '{}' is already registered", name);
            }

            eprintln!("Generating signing keypair...");
            let (signing_priv, signing_pub) = generate_keypair()
                .map_err(|e| anyhow::anyhow!("generate signing keypair: {e}"))?;

            eprintln!("Generating deploy keypair...");
            let (deploy_priv, deploy_pub) = generate_keypair()
                .map_err(|e| anyhow::anyhow!("generate deploy keypair: {e}"))?;

            // Optionally register deploy key with Gitea.
            let gitea_key_id: u64 = if no_gitea {
                eprintln!("Skipping Gitea deploy key registration (--no-gitea).");
                0
            } else {
                let client = load_gitea_client(gitea_url, gitea_token, owner, repo)?;
                let key_title = format!("relicario-{}", name);
                eprintln!("Registering deploy key '{}' with Gitea...", key_title);
                client.create_deploy_key(&key_title, &deploy_pub)?
            };

            // Store keys locally with proper permissions.
            crate::device::store_device_keys(
                &name,
                &signing_priv,
                &signing_pub,
                &deploy_priv,
                &deploy_pub,
                gitea_key_id,
            )?;

            // Mark as current device.
            crate::device::set_current_device(&name)?;

            // Configure git signing + SSH deploy key in the vault repo.
            crate::device::configure_git_signing(&root, &name)?;

            // Update devices.json.
            let current_name = name.clone();
            let mut devices = existing;
            devices.push(DeviceEntry {
                name: name.clone(),
                public_key: signing_pub.clone(),
                added_at: relicario_core::now_unix(),
                added_by: current_name,
            });
            fs::create_dir_all(&relicario_dir)?;
            fs::write(&devices_path, serde_json::to_string_pretty(&devices)?)?;

            // Commit the update.
            crate::helpers::git_run(
                &root,
                &["add", ".relicario/devices.json"],
                &format!("device register \"{name}\": git add .relicario/devices.json"),
            )?;
            let msg = format!("device: register {}", name);
            crate::helpers::git_run(
                &root,
                &["commit", "-m", &msg],
                &format!("device register \"{name}\": git commit"),
            )?;

            eprintln!("Device '{}' registered.", name);
            eprintln!("Signing public key:");
            eprintln!("  {}", signing_pub);
            if gitea_key_id != 0 {
                eprintln!("Gitea deploy key ID: {}", gitea_key_id);
            }
            Ok(())
        }

        DeviceAction::Revoke { name } => {
            // Guard: refuse to revoke the currently active device (would lock
            // the user out).  They must add another device first.
            if let Some(current) = crate::device::current_device()? {
                if current == name {
                    anyhow::bail!(
                        "cannot revoke the current device '{}' — you would lose \
                         push access. Register another device first.",
                        name
                    );
                }
            }

            // Load devices.json.
            let mut devices: Vec<DeviceEntry> = fs::read(&devices_path)
                .ok()
                .and_then(|b| serde_json::from_slice(&b).ok())
                .unwrap_or_default();

            let device = devices
                .iter()
                .find(|d| d.name == name)
                .ok_or_else(|| anyhow::anyhow!("device '{}' not found", name))?
                .clone();

            // Remove from devices.json.
            devices.retain(|d| d.name != name);
            fs::write(&devices_path, serde_json::to_string_pretty(&devices)?)?;

            // Append to revoked.json.
            let revoked_path = relicario_dir.join("revoked.json");
            let mut revoked: Vec<RevokedEntry> = fs::read(&revoked_path)
                .ok()
                .and_then(|b| serde_json::from_slice(&b).ok())
                .unwrap_or_default();

            let revoked_by = crate::device::current_device()?
                .unwrap_or_else(|| "unknown".to_string());

            revoked.push(RevokedEntry {
                name: name.clone(),
                public_key: device.public_key.clone(),
                revoked_at: relicario_core::now_unix(),
                revoked_by,
            });
            fs::write(&revoked_path, serde_json::to_string_pretty(&revoked)?)?;

            // Delete deploy key from Gitea (best-effort — don't fail if it
            // was already deleted or the config is missing).
            if let Ok(key_id) = crate::device::load_gitea_key_id(&name) {
                if key_id != 0 {
                    // Build client from env vars only (no flags in revoke).
                    match load_gitea_client(None, None, None, None) {
                        Ok(client) => {
                            if let Err(e) = client.delete_deploy_key(key_id) {
                                eprintln!(
                                    "warning: failed to delete Gitea deploy key {}: {}",
                                    key_id, e
                                );
                            } else {
                                eprintln!("Deleted Gitea deploy key {}.", key_id);
                            }
                        }
                        Err(_) => {
                            eprintln!(
                                "warning: Gitea env vars not set — deploy key {} \
                                 not deleted from Gitea.",
                                key_id
                            );
                        }
                    }
                }
            }

            // Commit devices.json + revoked.json (always both — revoked.json
            // was just written above so it is guaranteed to exist).
            let add_args = [
                "add",
                ".relicario/devices.json",
                ".relicario/revoked.json",
            ];
            crate::helpers::git_run(
                &root,
                &add_args,
                &format!("device revoke \"{name}\": git add devices.json + revoked.json"),
            )?;
            let msg = format!("device: revoke {}", name);
            crate::helpers::git_run(
                &root,
                &["commit", "-m", &msg],
                &format!("device revoke \"{name}\": git commit"),
            )?;

            eprintln!("Device '{}' revoked.", name);
            eprintln!("Revoked signing key: {}", device.public_key);
            Ok(())
        }

        DeviceAction::List => {
            let devices: Vec<DeviceEntry> = fs::read(&devices_path)
                .ok()
                .and_then(|b| serde_json::from_slice(&b).ok())
                .unwrap_or_default();

            let current = crate::device::current_device()?.unwrap_or_default();

            if devices.is_empty() {
                println!("No registered devices.");
                return Ok(());
            }

            println!("{:<20} {:<20} SIGNING KEY (prefix)", "NAME", "ADDED");
            println!("{}", "-".repeat(72));
            for d in &devices {
                let marker = if d.name == current { " *" } else { "" };
                let added = crate::helpers::iso8601(d.added_at);
                // Show only the first 40 chars of the public key line for readability.
                let key_prefix: String = d.public_key.chars().take(40).collect();
                println!("{:<20} {:<20} {}{}",
                    d.name, added, key_prefix, marker);
            }
            if !current.is_empty() {
                println!("\n* = current device");
            }
            Ok(())
        }
    }
}
