//! Runtime session state for the interactive REPL.
//!
//! Wraps the legacy `StoredState` (identity + hybrid key) and adds
//! multi-conversation management via `ConversationStore`.

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;

use anyhow::Context;
use zeroize::Zeroizing;

use quicproquo_core::{DiskKeyStore, GroupMember, HybridKeypair, IdentityKeypair};

use super::conversation::{
    now_ms, Conversation, ConversationId, ConversationKind, ConversationStore,
};
use super::state::{load_or_init_state, keystore_path};

/// Runtime state for an interactive REPL session.
pub struct SessionState {
    /// Long-term identity keypair.
    pub identity: Arc<IdentityKeypair>,
    /// Post-quantum hybrid keypair.
    pub hybrid_kp: Option<HybridKeypair>,
    /// Path to the legacy state file (for backward compat with one-shot commands).
    pub state_path: PathBuf,
    /// Optional password for the legacy state file. Zeroized on drop. (M9)
    pub password: Option<Zeroizing<String>>,
    /// SQLite-backed conversation + message store.
    pub conv_store: ConversationStore,
    /// Currently active conversation.
    pub active_conversation: Option<ConversationId>,
    /// In-memory GroupMember instances keyed by conversation ID.
    pub members: HashMap<ConversationId, GroupMember>,
    /// Holds the GroupMember whose KeyPackage was uploaded to the server.
    /// Its keystore contains the HPKE init private key needed to decrypt
    /// incoming Welcome messages. Consumed on auto-join, then replenished.
    pub pending_member: Option<GroupMember>,
    /// Whether to display typing indicators from others (session preference).
    pub typing_notify_enabled: bool,
    /// Tracks who is currently typing and when the indicator was last received.
    /// Entries older than 10 seconds are considered expired.
    pub typing_indicators: HashMap<String, Instant>,
    /// Per-conversation disappearing message TTL in seconds. None = messages persist.
    pub disappear_ttl: HashMap<ConversationId, u32>,
    /// When true, /members and /group-info redact identity keys as `[redacted-XXXX]`.
    pub redact_keys: bool,
    /// When Some(secs), auto-clear local messages older than this duration.
    pub auto_clear_secs: Option<u32>,
    /// When true, send periodic dummy messages for traffic analysis resistance.
    pub padding_enabled: bool,
    /// Last epoch at which we sent a message (for /verify-fs).
    pub last_send_epoch: Option<u64>,
}

impl SessionState {
    /// Load identity from the legacy state file, open the conversation store,
    /// and migrate any existing single-group state into the conversation DB.
    pub fn load(
        state_path: &Path,
        password: Option<&str>,
    ) -> anyhow::Result<Self> {
        let state = load_or_init_state(state_path, password)?;

        let identity = Arc::new(IdentityKeypair::from_seed(state.identity_seed));
        let hybrid_kp = state
            .hybrid_key
            .as_ref()
            .map(HybridKeypair::from_bytes)
            .transpose()
            .context("decode hybrid key")?;

        // Open the conversation DB next to the state file.
        // When a state password is provided, encrypt the DB with SQLCipher.
        let db_path = state_path.with_extension("convdb");
        let conv_store = ConversationStore::open(&db_path, password)?;

        let mut session = Self {
            identity,
            hybrid_kp,
            state_path: state_path.to_path_buf(),
            password: password.map(|p| Zeroizing::new(String::from(p))),
            conv_store,
            active_conversation: None,
            members: HashMap::new(),
            pending_member: None,
            typing_notify_enabled: true,
            typing_indicators: HashMap::new(),
            disappear_ttl: HashMap::new(),
            redact_keys: false,
            auto_clear_secs: None,
            padding_enabled: false,
            last_send_epoch: None,
        };

        // Migrate legacy single-group into conversations if present and not yet migrated.
        if state.group.is_some() {
            session.migrate_legacy_group(state_path, &state.group)?;
        }

        // Load all existing conversations' GroupMembers into memory.
        session.load_all_members()?;

        Ok(session)
    }

    /// Migrate the legacy single-group from StoredState into the conversation DB.
    fn migrate_legacy_group(
        &mut self,
        state_path: &Path,
        group_blob: &Option<Vec<u8>>,
    ) -> anyhow::Result<()> {
        let blob = match group_blob {
            Some(b) => b,
            None => return Ok(()),
        };

        // Reconstruct GroupMember using the legacy keystore and group blob.
        let ks_path = keystore_path(state_path);
        let ks = DiskKeyStore::persistent(&ks_path)?;
        let group = bincode::deserialize(blob).context("decode legacy group")?;
        let member = GroupMember::new_with_state(
            Arc::clone(&self.identity),
            ks,
            Some(group),
            false, // legacy groups are classical
        );

        let group_id_bytes = member.group_id().unwrap_or_default();

        // Use the first 16 bytes of the group_id as the ConversationId.
        let conv_id = if group_id_bytes.len() >= 16 {
            ConversationId::from_slice(&group_id_bytes[..16])
                .unwrap_or(ConversationId([0; 16]))
        } else {
            ConversationId::from_group_name(&hex::encode(&group_id_bytes))
        };

        // Check if already migrated.
        if self.conv_store.load_conversation(&conv_id)?.is_some() {
            return Ok(());
        }

        let member_keys = member.member_identities();
        let short_id = &hex::encode(&group_id_bytes)[..8.min(group_id_bytes.len() * 2)];
        let conv = Conversation {
            id: conv_id.clone(),
            kind: ConversationKind::Group {
                name: format!("legacy-{short_id}"),
            },
            display_name: format!("legacy-{short_id}"),
            mls_group_blob: Some(blob.clone()),
            keystore_blob: None,
            member_keys,
            unread_count: 0,
            last_activity_ms: now_ms(),
            created_at_ms: now_ms(),
            is_hybrid: false,
            last_seen_seq: 0,
        };

        self.conv_store.save_conversation(&conv)?;
        self.members.insert(conv_id, member);

        Ok(())
    }

    /// Load all conversations from the DB and create in-memory GroupMember instances.
    fn load_all_members(&mut self) -> anyhow::Result<()> {
        let convs = self.conv_store.list_conversations()?;
        for conv in convs {
            if self.members.contains_key(&conv.id) {
                continue;
            }
            let member = self.create_member_from_conv(&conv)?;
            self.members.insert(conv.id.clone(), member);
        }
        Ok(())
    }

    /// Create a GroupMember from a stored conversation.
    fn create_member_from_conv(&self, conv: &Conversation) -> anyhow::Result<GroupMember> {
        let ks_path = self.keystore_path_for(&conv.id);
        let ks = DiskKeyStore::persistent(&ks_path)
            .unwrap_or_else(|e| {
                tracing::warn!(path = %ks_path.display(), error = %e, "DiskKeyStore open failed, falling back to ephemeral");
                DiskKeyStore::ephemeral()
            });

        let group = conv
            .mls_group_blob
            .as_ref()
            .map(|b| bincode::deserialize(b))
            .transpose()
            .context("decode MLS group from conversation db")?;

        Ok(GroupMember::new_with_state(
            Arc::clone(&self.identity),
            ks,
            group,
            conv.is_hybrid,
        ))
    }

    /// Path for a per-conversation keystore file.
    fn keystore_path_for(&self, conv_id: &ConversationId) -> PathBuf {
        let dir = self.state_path.with_extension("keystores");
        dir.join(format!("{}.ks", conv_id.hex()))
    }

    /// Persist a conversation's MLS group state back to the DB.
    pub fn save_member(&self, conv_id: &ConversationId) -> anyhow::Result<()> {
        let member = self.members.get(conv_id).context("no such conversation")?;
        let blob = member
            .group_ref()
            .map(bincode::serialize)
            .transpose()
            .context("serialize MLS group")?;

        let member_keys = member.member_identities();

        // Update the mls_group_blob and member_keys in the DB.
        if let Some(mut conv) = self.conv_store.load_conversation(conv_id)? {
            conv.mls_group_blob = blob;
            conv.member_keys = member_keys;
            self.conv_store.save_conversation(&conv)?;
        }

        Ok(())
    }

    /// Persist all in-memory group states back to the DB.
    pub fn save_all(&self) -> anyhow::Result<()> {
        for conv_id in self.members.keys() {
            if let Err(e) = self.save_member(conv_id) {
                tracing::warn!(conv = %conv_id.hex(), error = %e, "failed to save conversation");
            }
        }
        Ok(())
    }

    /// Add a new conversation and its GroupMember to the session.
    pub fn add_conversation(
        &mut self,
        conv: Conversation,
        member: GroupMember,
    ) -> anyhow::Result<()> {
        // Ensure keystore directory exists
        let ks_path = self.keystore_path_for(&conv.id);
        if let Some(parent) = ks_path.parent() {
            std::fs::create_dir_all(parent).ok();
        }

        self.conv_store.save_conversation(&conv)?;
        self.members.insert(conv.id.clone(), member);
        Ok(())
    }

    /// Get a mutable reference to a conversation's GroupMember.
    pub fn get_member_mut(&mut self, conv_id: &ConversationId) -> Option<&mut GroupMember> {
        self.members.get_mut(conv_id)
    }

    /// Public key bytes for this identity.
    pub fn identity_bytes(&self) -> Vec<u8> {
        self.identity.public_key_bytes().to_vec()
    }

    /// Short hex prefix of the identity key for display.
    pub fn identity_short(&self) -> String {
        hex::encode(&self.identity.public_key_bytes()[..4])
    }

    /// Get display name of a conversation.
    pub fn active_display_name(&self) -> Option<String> {
        let id = self.active_conversation.as_ref()?;
        self.conv_store.load_conversation(id).ok().flatten().map(|c| c.display_name)
    }

    /// Count total unread across all conversations.
    pub fn total_unread(&self) -> u32 {
        self.conv_store
            .list_conversations()
            .unwrap_or_default()
            .iter()
            .map(|c| c.unread_count)
            .sum()
    }
}
