//! Multi-identity management: create, list, switch, delete identities.
//! Each identity gets its own data directory with identity.key, itsgoin.db, and blobs/.
//! One identity is active at a time — hot-swap via Node teardown/rebuild.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use tracing::{info, warn};

use crate::node::Node;
use crate::types::NodeId;

/// Metadata about a stored identity.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentityInfo {
    pub node_id: NodeId,
    pub node_id_hex: String,
    pub display_name: String,
    pub created_at: u64,
    pub last_used_at: u64,
}

/// Per-identity metadata stored in meta.json.
#[derive(Debug, Clone, Serialize, Deserialize)]
struct IdentityMeta {
    display_name: String,
    created_at: u64,
    last_used_at: u64,
}

/// Manages multiple identities on a single device.
/// One identity is active at a time. Switching tears down the current Node
/// and builds a new one from the target identity's data directory.
pub struct IdentityManager {
    base_dir: PathBuf,
    active_node: Option<Arc<Node>>,
    active_id: Option<NodeId>,
    bind_addr: Option<std::net::SocketAddr>,
    profile: crate::types::DeviceProfile,
}

impl IdentityManager {
    /// Create a new IdentityManager rooted at the given base directory.
    /// Performs legacy layout migration if needed (flat itsgoin-data/ → per-identity subdirs).
    pub async fn open(
        base_dir: impl AsRef<Path>,
        bind_addr: Option<std::net::SocketAddr>,
        profile: crate::types::DeviceProfile,
    ) -> anyhow::Result<Self> {
        let base_dir = base_dir.as_ref().to_path_buf();
        std::fs::create_dir_all(&base_dir)?;

        let mut mgr = Self {
            base_dir: base_dir.clone(),
            active_node: None,
            active_id: None,
            bind_addr,
            profile,
        };

        // Migrate legacy flat layout if needed
        mgr.migrate_legacy_layout()?;

        // Ensure identities/ directory exists
        let identities_dir = base_dir.join("identities");
        std::fs::create_dir_all(&identities_dir)?;

        // Auto-start the active identity (if any)
        if let Some(active_id) = mgr.read_active_identity()? {
            let id_dir = mgr.identity_dir(&active_id);
            if id_dir.exists() {
                info!(identity = hex::encode(active_id), "Starting active identity");
                let node = Arc::new(
                    Node::open_with_bind(&id_dir, bind_addr, profile).await?
                );
                mgr.active_id = Some(active_id);
                mgr.active_node = Some(node);
                mgr.update_last_used(&active_id)?;
            } else {
                warn!(identity = hex::encode(active_id), "Active identity directory missing");
            }
        } else {
            // No active identity — check if there's exactly one, auto-select it
            let identities = mgr.list_identities()?;
            if identities.len() == 1 {
                let id = identities[0].node_id;
                let id_dir = mgr.identity_dir(&id);
                info!(identity = hex::encode(id), "Auto-selecting only available identity");
                let node = Arc::new(
                    Node::open_with_bind(&id_dir, bind_addr, profile).await?
                );
                mgr.active_id = Some(id);
                mgr.active_node = Some(node);
                mgr.write_active_identity(&id)?;
                mgr.update_last_used(&id)?;
            }
        }

        Ok(mgr)
    }

    /// Get the base data directory.
    pub fn base_dir(&self) -> &Path {
        &self.base_dir
    }

    /// Get the currently active Node, if any.
    pub fn active_node(&self) -> Option<&Arc<Node>> {
        self.active_node.as_ref()
    }

    /// Get the currently active Node, or error.
    pub fn node(&self) -> anyhow::Result<&Arc<Node>> {
        self.active_node.as_ref()
            .ok_or_else(|| anyhow::anyhow!("No active identity"))
    }

    /// Get the active identity's NodeId.
    pub fn active_id(&self) -> Option<NodeId> {
        self.active_id
    }

    // --- Identity CRUD ---

    /// List all available identities.
    pub fn list_identities(&self) -> anyhow::Result<Vec<IdentityInfo>> {
        let identities_dir = self.base_dir.join("identities");
        if !identities_dir.exists() {
            return Ok(vec![]);
        }

        let mut result = Vec::new();
        for entry in std::fs::read_dir(&identities_dir)? {
            let entry = entry?;
            if !entry.file_type()?.is_dir() {
                continue;
            }
            let dir_name = entry.file_name().to_string_lossy().to_string();
            let key_path = entry.path().join("identity.key");
            if !key_path.exists() {
                continue;
            }

            // Derive node_id from key
            let key_bytes = std::fs::read(&key_path)?;
            let seed: [u8; 32] = key_bytes.try_into()
                .map_err(|_| anyhow::anyhow!("invalid key in {}", dir_name))?;
            let secret_key = iroh::SecretKey::from_bytes(&seed);
            let node_id: NodeId = *secret_key.public().as_bytes();
            let node_id_hex = hex::encode(node_id);

            // Read metadata
            let meta_path = entry.path().join("meta.json");
            let meta = if meta_path.exists() {
                serde_json::from_str::<IdentityMeta>(
                    &std::fs::read_to_string(&meta_path)?
                ).unwrap_or(IdentityMeta {
                    display_name: node_id_hex[..12].to_string(),
                    created_at: 0,
                    last_used_at: 0,
                })
            } else {
                IdentityMeta {
                    display_name: node_id_hex[..12].to_string(),
                    created_at: 0,
                    last_used_at: 0,
                }
            };

            result.push(IdentityInfo {
                node_id,
                node_id_hex,
                display_name: meta.display_name,
                created_at: meta.created_at,
                last_used_at: meta.last_used_at,
            });
        }

        // Sort by last_used descending
        result.sort_by(|a, b| b.last_used_at.cmp(&a.last_used_at));
        Ok(result)
    }

    /// Create a new identity with the given display name.
    /// Returns the new NodeId.
    pub fn create_identity(&self, name: &str) -> anyhow::Result<NodeId> {
        let key = iroh::SecretKey::generate(&mut rand::rng());
        let seed = key.to_bytes();
        let node_id: NodeId = *key.public().as_bytes();
        let node_id_hex = hex::encode(node_id);

        let id_dir = self.base_dir.join("identities").join(&node_id_hex);
        std::fs::create_dir_all(&id_dir)?;

        // Write identity key with restricted permissions
        let key_path = id_dir.join("identity.key");
        std::fs::write(&key_path, seed)?;
        set_key_permissions(&key_path);

        // Write metadata
        let now = now_ms();
        let meta = IdentityMeta {
            display_name: name.to_string(),
            created_at: now,
            last_used_at: now,
        };
        std::fs::write(
            id_dir.join("meta.json"),
            serde_json::to_string_pretty(&meta)?,
        )?;

        info!(identity = node_id_hex, name, "Created new identity");
        Ok(node_id)
    }

    /// Import an identity from a hex-encoded secret key.
    pub fn import_identity_from_key(&self, key_hex: &str, name: &str) -> anyhow::Result<NodeId> {
        let seed_bytes = hex::decode(key_hex)?;
        let seed: [u8; 32] = seed_bytes.try_into()
            .map_err(|_| anyhow::anyhow!("key must be exactly 32 bytes (64 hex chars)"))?;

        let secret_key = iroh::SecretKey::from_bytes(&seed);
        let node_id: NodeId = *secret_key.public().as_bytes();
        let node_id_hex = hex::encode(node_id);

        let id_dir = self.base_dir.join("identities").join(&node_id_hex);
        if id_dir.exists() {
            anyhow::bail!("Identity {} already exists", &node_id_hex[..12]);
        }

        std::fs::create_dir_all(&id_dir)?;
        let key_path = id_dir.join("identity.key");
        std::fs::write(&key_path, seed)?;
        set_key_permissions(&key_path);

        let now = now_ms();
        let meta = IdentityMeta {
            display_name: name.to_string(),
            created_at: now,
            last_used_at: now,
        };
        std::fs::write(
            id_dir.join("meta.json"),
            serde_json::to_string_pretty(&meta)?,
        )?;

        info!(identity = node_id_hex, name, "Imported identity from key");
        Ok(node_id)
    }

    /// Switch to a different identity. Shuts down the current Node and starts a new one.
    pub async fn switch_identity(&mut self, target_id: &NodeId) -> anyhow::Result<Arc<Node>> {
        let target_hex = hex::encode(target_id);
        let id_dir = self.identity_dir(target_id);
        if !id_dir.exists() {
            anyhow::bail!("Identity {} not found", &target_hex[..12]);
        }

        // Shutdown current node before switching
        if let Some(ref node) = self.active_node {
            info!(old = hex::encode(node.node_id), new = target_hex, "Switching identity — shutting down old node");
            node.network.shutdown_ref().await;
        }
        self.active_node = None;
        self.active_id = None;

        // Start new node
        let node = Arc::new(
            Node::open_with_bind(&id_dir, self.bind_addr, self.profile).await?
        );
        let node_arc = Arc::clone(&node);
        self.active_node = Some(node);
        self.active_id = Some(*target_id);
        self.write_active_identity(target_id)?;
        self.update_last_used(target_id)?;

        info!(identity = target_hex, "Identity switch complete");
        Ok(node_arc)
    }

    /// Delete an identity. Refuses to delete the active identity.
    pub fn delete_identity(&self, target_id: &NodeId) -> anyhow::Result<()> {
        if self.active_id.as_ref() == Some(target_id) {
            anyhow::bail!("Cannot delete the active identity — switch to another first");
        }

        let id_dir = self.identity_dir(target_id);
        if !id_dir.exists() {
            anyhow::bail!("Identity not found");
        }

        std::fs::remove_dir_all(&id_dir)?;
        info!(identity = hex::encode(target_id), "Deleted identity");
        Ok(())
    }

    // --- Internal helpers ---

    fn identity_dir(&self, node_id: &NodeId) -> PathBuf {
        self.base_dir.join("identities").join(hex::encode(node_id))
    }

    fn read_active_identity(&self) -> anyhow::Result<Option<NodeId>> {
        let path = self.base_dir.join("active_identity");
        if !path.exists() {
            return Ok(None);
        }
        let hex_str = std::fs::read_to_string(&path)?.trim().to_string();
        if hex_str.is_empty() {
            return Ok(None);
        }
        let bytes = hex::decode(&hex_str)?;
        let id: NodeId = bytes.try_into()
            .map_err(|_| anyhow::anyhow!("invalid active_identity file"))?;
        Ok(Some(id))
    }

    fn write_active_identity(&self, node_id: &NodeId) -> anyhow::Result<()> {
        let path = self.base_dir.join("active_identity");
        std::fs::write(&path, hex::encode(node_id))?;
        Ok(())
    }

    fn update_last_used(&self, node_id: &NodeId) -> anyhow::Result<()> {
        let meta_path = self.identity_dir(node_id).join("meta.json");
        if meta_path.exists() {
            let mut meta: IdentityMeta = serde_json::from_str(
                &std::fs::read_to_string(&meta_path)?
            )?;
            meta.last_used_at = now_ms();
            std::fs::write(&meta_path, serde_json::to_string_pretty(&meta)?)?;
        }
        Ok(())
    }

    /// Migrate from flat itsgoin-data/ layout to per-identity subdirectories.
    /// Only runs if identity.key exists in the base_dir (legacy layout).
    fn migrate_legacy_layout(&self) -> anyhow::Result<()> {
        let legacy_key = self.base_dir.join("identity.key");
        if !legacy_key.exists() {
            return Ok(()); // Already migrated or fresh install
        }

        info!("Migrating legacy flat layout to per-identity directories");

        // Read the key to determine node_id
        let key_bytes = std::fs::read(&legacy_key)?;
        let seed: [u8; 32] = key_bytes.try_into()
            .map_err(|_| anyhow::anyhow!("invalid legacy identity.key"))?;
        let secret_key = iroh::SecretKey::from_bytes(&seed);
        let node_id: NodeId = *secret_key.public().as_bytes();
        let node_id_hex = hex::encode(node_id);

        let id_dir = self.base_dir.join("identities").join(&node_id_hex);
        std::fs::create_dir_all(&id_dir)?;

        // Move files
        let files_to_move = ["identity.key", "itsgoin.db", "itsgoin.db-wal", "itsgoin.db-shm", "bootstrap.json"];
        for name in &files_to_move {
            let src = self.base_dir.join(name);
            if src.exists() {
                let dst = id_dir.join(name);
                std::fs::rename(&src, &dst)?;
            }
        }

        // Move blobs directory
        let blobs_src = self.base_dir.join("blobs");
        if blobs_src.exists() {
            let blobs_dst = id_dir.join("blobs");
            std::fs::rename(&blobs_src, &blobs_dst)?;
        }

        // Write metadata
        let now = now_ms();
        let meta = IdentityMeta {
            display_name: node_id_hex[..12].to_string(),
            created_at: now,
            last_used_at: now,
        };
        std::fs::write(
            id_dir.join("meta.json"),
            serde_json::to_string_pretty(&meta)?,
        )?;

        // Write active identity
        self.write_active_identity(&node_id)?;

        info!(identity = node_id_hex, "Legacy migration complete");
        Ok(())
    }
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as u64
}

/// Set restrictive permissions on identity key files (user-only read/write).
fn set_key_permissions(path: &std::path::Path) {
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
    }
    #[cfg(not(unix))]
    {
        let _ = path; // no-op on non-Unix (Windows uses ACLs, Android sandboxes)
    }
}
