//! Shared application state managed by Tauri.

use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use tauri::{AppHandle, Manager, Wry};
use tokio::sync::{oneshot, Mutex as AsyncMutex};

use crate::agent::artifact::ArtifactStore;
use crate::agent::jobs::BackgroundJobCoordinator;
use crate::agent::{AgentService, AgentSettingsService};
use crate::error::AppError;
use crate::mappings::{MappingsService, TauriMappingsEventEmitter};
use crate::news::NewsService;
use crate::portfolio::PortfolioService;
use crate::research::ResearchService;
use crate::terminal::google_finance::GoogleFinanceLookup;
use crate::terminal::sec_edgar::{
    LiveSecFetcher, SecEdgarClient, SecEdgarLookup, SecUserAgentProvider,
};
use crate::terminal::security_lookup::SecurityLookup;
use crate::terminal::types::AvailableXbrlConcept;
use crate::terminal::TerminalCommandService;

struct PendingApprovalEntry {
    request_id: Option<String>,
    sender: oneshot::Sender<bool>,
}

pub struct PendingAgentToolApprovals {
    next_approval_id: AtomicU64,
    senders: Mutex<HashMap<String, PendingApprovalEntry>>,
}

impl PendingAgentToolApprovals {
    pub fn new() -> Self {
        Self {
            next_approval_id: AtomicU64::new(1),
            senders: Mutex::new(HashMap::new()),
        }
    }

    pub fn register_for_request(
        &self,
        request_id: Option<String>,
    ) -> Result<(String, oneshot::Receiver<bool>), AppError> {
        let approval_id = format!(
            "approval-{}",
            self.next_approval_id.fetch_add(1, Ordering::Relaxed)
        );
        let (sender, receiver) = oneshot::channel();
        let mut senders = self
            .senders
            .lock()
            .map_err(|_| AppError::InvalidSettings("approval state is unavailable".to_string()))?;
        senders.insert(
            approval_id.clone(),
            PendingApprovalEntry { request_id, sender },
        );
        Ok((approval_id, receiver))
    }

    pub fn resolve(&self, approval_id: &str, approved: bool) -> Result<(), AppError> {
        let entry = self
            .senders
            .lock()
            .map_err(|_| AppError::InvalidSettings("approval state is unavailable".to_string()))?
            .remove(approval_id)
            .ok_or_else(|| AppError::AgentToolApprovalNotFound(approval_id.to_string()))?;

        entry
            .sender
            .send(approved)
            .map_err(|_| AppError::AgentToolApprovalNotFound(approval_id.to_string()))
    }

    pub fn cancel(&self, approval_id: &str) {
        if let Ok(mut senders) = self.senders.lock() {
            senders.remove(approval_id);
        }
    }

    pub fn cancel_for_request(&self, request_id: &str) {
        if let Ok(mut senders) = self.senders.lock() {
            senders.retain(|_, entry| {
                entry
                    .request_id
                    .as_deref()
                    .is_none_or(|entry_request_id| entry_request_id != request_id)
            });
        }
    }
}

impl Default for PendingAgentToolApprovals {
    fn default() -> Self {
        Self::new()
    }
}

pub struct ActiveChatStreams {
    senders: Mutex<HashMap<String, oneshot::Sender<()>>>,
}

impl ActiveChatStreams {
    pub fn new() -> Self {
        Self {
            senders: Mutex::new(HashMap::new()),
        }
    }

    pub fn register(
        &self,
        request_id: String,
        sender: oneshot::Sender<()>,
    ) -> Result<(), AppError> {
        let mut senders = self.senders.lock().map_err(|_| {
            AppError::InvalidSettings("active stream state is unavailable".to_string())
        })?;
        senders.insert(request_id, sender);
        Ok(())
    }

    pub fn cancel(&self, request_id: &str) -> Result<bool, AppError> {
        let sender = self
            .senders
            .lock()
            .map_err(|_| {
                AppError::InvalidSettings("active stream state is unavailable".to_string())
            })?
            .remove(request_id);

        Ok(sender.is_some_and(|sender| sender.send(()).is_ok()))
    }

    pub fn remove(&self, request_id: &str) -> Result<(), AppError> {
        self.senders
            .lock()
            .map_err(|_| {
                AppError::InvalidSettings("active stream state is unavailable".to_string())
            })?
            .remove(request_id);
        Ok(())
    }
}

impl Default for ActiveChatStreams {
    fn default() -> Self {
        Self::new()
    }
}

struct SettingsBackedSecUserAgentProvider {
    settings: AgentSettingsService<Wry>,
}

impl SecUserAgentProvider for SettingsBackedSecUserAgentProvider {
    fn user_agent(&self) -> Option<String> {
        self.settings
            .load()
            .ok()
            .and_then(|settings| {
                let value = settings.sec_edgar_user_agent.trim().to_string();
                if value.is_empty() {
                    None
                } else {
                    Some(value)
                }
            })
            .or_else(|| std::env::var("SEC_EDGAR_USER_AGENT").ok())
    }
}

/// Runtime services shared across Tauri commands.
pub struct AppState {
    /// Stateful chat service used for per-session conversation history and agent config.
    pub agent: AsyncMutex<AgentService<Wry>>,
    /// Local-first RSS/Atom news runtime backed by SQLite.
    pub news_service: Arc<NewsService>,
    /// Slash-command executor backed by shared services.
    pub command_service: Arc<TerminalCommandService>,
    /// Equity research note, graph, and ghost writer runtime.
    pub research_service: Arc<ResearchService<Wry>>,
    /// Custom XBRL concept mappings service.
    pub mappings_service: Arc<MappingsService>,
    /// SEC EDGAR lookup shared between command service and mapping commands.
    pub edgar_lookup: Arc<SecEdgarLookup>,
    /// Cache of XBRL concepts extracted from filings, keyed by uppercase ticker.
    pub concept_cache: Arc<Mutex<HashMap<String, Vec<AvailableXbrlConcept>>>>,
    /// Active chat streams keyed by request id for cancellation.
    pub active_chat_streams: Arc<ActiveChatStreams>,
    /// Pending approvals for agent-triggered mutating commands.
    pub pending_agent_tool_approvals: Arc<PendingAgentToolApprovals>,
    /// Bounded artifact store for agent-generated files.
    pub artifact_store: Arc<ArtifactStore>,
    /// Background job coordinator for non-interactive agent workflows.
    pub background_jobs: Arc<BackgroundJobCoordinator<Wry>>,
}

impl AppState {
    /// Create a new application state for the current Tauri app.
    pub fn new(app_handle: &AppHandle<Wry>) -> Result<Self, AppError> {
        let sec_user_agent_provider = Arc::new(SettingsBackedSecUserAgentProvider {
            settings: AgentSettingsService::new(app_handle),
        });
        let security_lookup: Arc<dyn SecurityLookup> = Arc::new(GoogleFinanceLookup::default());
        let sec_edgar_client = Arc::new(SecEdgarClient::new(Box::new(LiveSecFetcher::new(
            sec_user_agent_provider,
        ))));
        let portfolio_service =
            Arc::new(PortfolioService::new(app_handle, security_lookup.clone()));
        let news_root = app_handle
            .path()
            .app_data_dir()
            .map_err(|_| {
                AppError::InvalidSettings("news app data directory is unavailable".to_string())
            })?
            .join("news");
        let news_service = Arc::new(
            NewsService::new(
                news_root.join("news.sqlite"),
                news_root.join("news-feeds.json"),
                include_bytes!("../news-feeds.default.json"),
            )
            .map_err(|error| AppError::InvalidSettings(error.to_string()))?,
        );
        let research_root = app_handle
            .path()
            .app_data_dir()
            .map_err(|_| {
                AppError::InvalidSettings("research app data directory is unavailable".to_string())
            })?
            .join("research");
        let research_service = Arc::new(
            ResearchService::new(app_handle, research_root.join("research.sqlite"))
                .map_err(|error| AppError::InvalidSettings(error.to_string()))?,
        );
        let mappings_root = app_handle.path().app_data_dir().map_err(|_| {
            AppError::InvalidSettings("mappings app data directory is unavailable".to_string())
        })?;
        let mappings_storage = crate::mappings::MappingsStorage::new(mappings_root)
            .map_err(|error| AppError::InvalidSettings(error.to_string()))?;
        let mappings_service = Arc::new(
            MappingsService::new(mappings_storage)
                .map_err(|error| AppError::InvalidSettings(error.to_string()))?,
        );
        let mappings_event_emitter = Arc::new(TauriMappingsEventEmitter::new(app_handle));
        let edgar_lookup = Arc::new(SecEdgarLookup::new(
            sec_edgar_client,
            mappings_service.clone(),
        ));
        let artifact_store = Arc::new(
            ArtifactStore::new(&app_handle.path().app_data_dir().map_err(|_| {
                AppError::InvalidSettings("artifact app data directory is unavailable".to_string())
            })?)
            .map_err(|error: crate::agent::artifact::ArtifactError| {
                AppError::InvalidSettings(error.to_string())
            })?,
        );
        let background_jobs = Arc::new(BackgroundJobCoordinator::new(app_handle.clone()));

        Ok(Self {
            agent: AsyncMutex::new(AgentService::new(app_handle)?),
            news_service: news_service.clone(),
            command_service: Arc::new(TerminalCommandService::new(
                security_lookup,
                edgar_lookup.clone(),
                portfolio_service,
                news_service,
                mappings_service.clone(),
                mappings_event_emitter,
            )),
            research_service,
            mappings_service,
            edgar_lookup,
            concept_cache: Arc::new(Mutex::new(HashMap::new())),
            active_chat_streams: Arc::new(ActiveChatStreams::new()),
            pending_agent_tool_approvals: Arc::new(PendingAgentToolApprovals::new()),
            artifact_store,
            background_jobs,
        })
    }
}

#[cfg(test)]
mod tests {
    use tokio::sync::oneshot;

    use super::{ActiveChatStreams, PendingAgentToolApprovals};

    #[test]
    fn cancel_for_request_drops_matching_pending_approvals() {
        let approvals = PendingAgentToolApprovals::new();
        let (_approval_id, receiver) = approvals
            .register_for_request(Some("request-1".to_string()))
            .expect("approval should register");

        approvals.cancel_for_request("request-1");

        assert!(receiver.blocking_recv().is_err());
    }

    #[test]
    fn cancel_chat_stream_removes_registered_sender() {
        let active_streams = ActiveChatStreams::new();
        let (cancel_sender, cancel_receiver) = oneshot::channel();

        active_streams
            .register("request-1".to_string(), cancel_sender)
            .expect("stream should register");

        assert!(active_streams
            .cancel("request-1")
            .expect("stream cancellation should succeed"));
        assert!(cancel_receiver.blocking_recv().is_ok());
        assert!(!active_streams
            .cancel("request-1")
            .expect("second cancellation should be a no-op"));
    }
}
