use std::sync::Arc;

use anyhow::{Context, Result};
use tracing;

use crate::db::SqliteDb;
use crate::types::{ScrollEventListener, ScrollStatus};
use super::credentials::CredentialStore;
use super::mapper::StatusMapper;
use super::mcp_client::McpConnectionManager;
use super::tool_mapping::ToolMapping;
use super::types::{ExternalRef, ExternalSystem, SyncDirection, SyncResult};

/// Bidirectional sync bridge between FURNACE scrolls and external systems.
///
/// Reads scroll data from SQLite, maps statuses via `StatusMapper`, and
/// manages external references. Actual HTTP calls to external APIs are
/// stubbed with TODO markers for now.
pub struct SyncBridge {
    sqlite: Arc<SqliteDb>,
    credentials: CredentialStore,
    mcp_client: McpConnectionManager,
}

impl SyncBridge {
    /// Create a new `SyncBridge` backed by the given SQLite database.
    pub fn new(sqlite: Arc<SqliteDb>) -> Self {
        Self {
            sqlite,
            credentials: CredentialStore::new(),
            mcp_client: McpConnectionManager::new(),
        }
    }

    /// Get a reference to the credential store (e.g. for configuring tokens).
    pub fn credentials(&self) -> &CredentialStore {
        &self.credentials
    }

    /// Get a reference to the MCP connection manager.
    pub fn mcp_client(&self) -> &McpConnectionManager {
        &self.mcp_client
    }

    /// Push a scroll's current state to an external system.
    ///
    /// Reads the scroll from SQLite, maps its status, and sends the update
    /// via the external system's MCP server. If no MCP server is registered
    /// for the system, returns an informative error.
    pub async fn push_to_external(
        &self,
        scroll_id: &str,
        system: &ExternalSystem,
    ) -> Result<SyncResult> {
        // Verify we have credentials
        let _token = self.credentials.get_token(system).context(format!(
            "No credentials configured for {}. Call credentials().store_token() first.",
            system
        ))?;

        // Read the scroll
        let scroll = self
            .sqlite
            .get_scroll(scroll_id)
            .await
            .context(format!("Failed to load scroll {}", scroll_id))?;

        // Map the status
        let external_status = StatusMapper::furnace_to_external(&scroll.status, system);
        tracing::info!(
            scroll_id = %scroll_id,
            system = %system,
            furnace_status = ?scroll.status,
            external_status = %external_status,
            "Pushing scroll to external system"
        );

        // Check if an MCP server is registered for this system
        if !self.mcp_client.is_registered(system).await {
            return Err(anyhow::anyhow!(
                "No MCP server registered for {}. Call mcp_client().register() first.",
                system
            ));
        }

        // Look up existing external ref to determine if this is an update
        let ext_ref = self.sqlite.get_external_ref(scroll_id, system).await?;

        let (tool_name, arguments) = match ext_ref {
            Some(ref r) => {
                // Existing link -- update the status on the external system
                ToolMapping::map_update_status(system, &r.external_id, &external_status)
            }
            None => {
                // No link yet -- create a new issue/page on the external system
                ToolMapping::map_create(system, &scroll.title, &scroll.intent, &external_status)
            }
        };

        let result = self
            .mcp_client
            .call_tool(system, tool_name, arguments)
            .await
            .context(format!(
                "MCP call '{}' failed for scroll {} on {}",
                tool_name, scroll_id, system
            ))?;

        // Try to extract an external ID from the response
        let external_id = result
            .get("id")
            .or_else(|| result.get("issue_key"))
            .or_else(|| result.get("page_id"))
            .and_then(|v| v.as_str())
            .map(|s| s.to_string())
            .or_else(|| ext_ref.map(|r| r.external_id));

        Ok(SyncResult {
            success: true,
            external_id,
            message: format!(
                "Pushed scroll {} to {} with status '{}'",
                scroll_id, system, external_status
            ),
        })
    }

    /// Pull an entity's current state from an external system into FURNACE.
    ///
    /// Fetches the issue/page from the external MCP server, maps its status
    /// via `StatusMapper::external_to_furnace`, and returns the result. If no
    /// MCP server is registered for the system, returns an informative error.
    pub async fn pull_from_external(
        &self,
        external_id: &str,
        system: &ExternalSystem,
    ) -> Result<SyncResult> {
        let _token = self.credentials.get_token(system).context(format!(
            "No credentials configured for {}",
            system
        ))?;

        tracing::info!(
            external_id = %external_id,
            system = %system,
            "Pulling from external system"
        );

        // Check if an MCP server is registered for this system
        if !self.mcp_client.is_registered(system).await {
            return Err(anyhow::anyhow!(
                "No MCP server registered for {}. Call mcp_client().register() first.",
                system
            ));
        }

        let (tool_name, arguments) = ToolMapping::map_get(system, external_id);

        let result = self
            .mcp_client
            .call_tool(system, tool_name, arguments)
            .await
            .context(format!(
                "MCP call '{}' failed for {} on {}",
                tool_name, external_id, system
            ))?;

        // Extract status from the response and map to FURNACE status
        let external_status = result
            .get("status")
            .and_then(|v| v.as_str())
            .unwrap_or("unknown");

        let furnace_status = StatusMapper::external_to_furnace(external_status);

        tracing::info!(
            external_id = %external_id,
            system = %system,
            external_status = %external_status,
            furnace_status = ?furnace_status,
            "Pulled and mapped status from external system"
        );

        Ok(SyncResult {
            success: true,
            external_id: Some(external_id.to_string()),
            message: format!(
                "Pulled {} from {}: external status '{}' -> {:?}",
                external_id, system, external_status, furnace_status
            ),
        })
    }

    /// Sync the status of a scroll with its linked external entity.
    ///
    /// Reads the scroll's current status, maps it, and returns the mapped
    /// value. Does not make external API calls yet.
    pub async fn sync_status(
        &self,
        scroll_id: &str,
        system: &ExternalSystem,
    ) -> Result<SyncResult> {
        let scroll = self
            .sqlite
            .get_scroll(scroll_id)
            .await
            .context(format!("Failed to load scroll {}", scroll_id))?;

        let external_status = StatusMapper::furnace_to_external(&scroll.status, system);

        tracing::info!(
            scroll_id = %scroll_id,
            system = %system,
            mapped_status = %external_status,
            "Status sync computed"
        );

        Ok(SyncResult {
            success: true,
            external_id: None,
            message: format!(
                "Status for scroll {}: {:?} -> {} ({})",
                scroll_id, scroll.status, external_status, system
            ),
        })
    }

    /// Link a FURNACE scroll to an external system entity by storing an
    /// `ExternalRef` in the database.
    pub async fn link_scroll(
        &self,
        scroll_id: &str,
        external_ref: &ExternalRef,
    ) -> Result<()> {
        self.sqlite
            .store_external_ref(scroll_id, external_ref)
            .await
            .context(format!(
                "Failed to link scroll {} to {} ({})",
                scroll_id, external_ref.external_id, external_ref.system
            ))?;

        tracing::info!(
            scroll_id = %scroll_id,
            system = %external_ref.system,
            external_id = %external_ref.external_id,
            "Scroll linked to external system"
        );

        Ok(())
    }

    /// Retrieve the external reference for a scroll, if one exists for the
    /// given system.
    pub async fn get_link(
        &self,
        scroll_id: &str,
        system: &ExternalSystem,
    ) -> Result<Option<ExternalRef>> {
        self.sqlite
            .get_external_ref(scroll_id, system)
            .await
    }

    /// Retrieve all external references, optionally filtered by scroll ID.
    ///
    /// Returns a list of `(scroll_id, ExternalRef)` tuples. When `scroll_id`
    /// is `None`, all links across all scrolls are returned.
    pub async fn get_all_links(
        &self,
        scroll_id: Option<&str>,
    ) -> Result<Vec<(String, ExternalRef)>> {
        self.sqlite
            .get_all_external_refs(scroll_id)
            .await
    }
}

#[async_trait::async_trait]
impl ScrollEventListener for SyncBridge {
    async fn on_scroll_status_changed(
        &self,
        scroll_id: &str,
        new_status: &ScrollStatus,
    ) {
        // Find all external links for this scroll.
        let links = match self.sqlite.get_all_external_refs(Some(scroll_id)).await {
            Ok(links) => links,
            Err(e) => {
                tracing::warn!(
                    scroll_id = %scroll_id,
                    error = %e,
                    "Failed to fetch external refs for auto-sync"
                );
                return;
            }
        };

        if links.is_empty() {
            tracing::debug!(
                scroll_id = %scroll_id,
                "No external links, skipping auto-sync"
            );
            return;
        }

        for (_sid, ext_ref) in &links {
            // Only push if sync direction allows it.
            match ext_ref.sync_direction {
                SyncDirection::Push | SyncDirection::Bidirectional => {}
                SyncDirection::Pull => {
                    tracing::debug!(
                        scroll_id = %scroll_id,
                        system = %ext_ref.system,
                        "Skipping auto-sync (direction is Pull-only)"
                    );
                    continue;
                }
            }

            tracing::info!(
                scroll_id = %scroll_id,
                system = %ext_ref.system,
                new_status = ?new_status,
                "Auto-syncing scroll status to external system"
            );

            match self.push_to_external(scroll_id, &ext_ref.system).await {
                Ok(result) => {
                    tracing::info!(
                        scroll_id = %scroll_id,
                        system = %ext_ref.system,
                        message = %result.message,
                        "Auto-sync push succeeded"
                    );
                }
                Err(e) => {
                    tracing::warn!(
                        scroll_id = %scroll_id,
                        system = %ext_ref.system,
                        error = %e,
                        "Auto-sync push failed (non-fatal)"
                    );
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;
    use tempfile::NamedTempFile;

    async fn make_bridge() -> (SyncBridge, Arc<SqliteDb>) {
        let tmp = NamedTempFile::new().expect("failed to create temp file");
        let sqlite = Arc::new(
            SqliteDb::new(tmp.path()).expect("failed to create test db"),
        );
        let bridge = SyncBridge::new(Arc::clone(&sqlite));
        (bridge, sqlite)
    }

    #[tokio::test]
    async fn test_push_without_credentials_fails() {
        let (bridge, _sqlite) = make_bridge().await;
        // No credentials stored -- push should error
        let result = bridge
            .push_to_external("scroll-1", &ExternalSystem::Linear)
            .await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_sync_status_for_existing_scroll() {
        let (bridge, sqlite) = make_bridge().await;

        // Insert a test scroll
        let scroll = crate::types::Scroll {
            id: "test-scroll-1".to_string(),
            title: "Test Scroll".to_string(),
            intent: "Testing sync".to_string(),
            constraints: vec![],
            tasks: vec![],
            metadata: crate::types::ScrollMetadata {
                execution_mode: crate::types::ExecutionMode::Forge,
                auto_forge_config: None,
                recipe_used: None,
                search_context: vec![],
            },
            status: crate::types::ScrollStatus::InProgress,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            cleanup_after: None,
        };
        sqlite.create_scroll(&scroll).await.unwrap();

        let result = bridge
            .sync_status("test-scroll-1", &ExternalSystem::Jira)
            .await
            .unwrap();

        assert!(result.success);
        assert!(result.message.contains("In Progress"));
    }

    #[tokio::test]
    async fn test_link_and_get_scroll() {
        let (bridge, sqlite) = make_bridge().await;

        // Insert a test scroll first (foreign key constraint)
        let scroll = crate::types::Scroll {
            id: "link-test-scroll".to_string(),
            title: "Link Test".to_string(),
            intent: "Testing link".to_string(),
            constraints: vec![],
            tasks: vec![],
            metadata: crate::types::ScrollMetadata {
                execution_mode: crate::types::ExecutionMode::Forge,
                auto_forge_config: None,
                recipe_used: None,
                search_context: vec![],
            },
            status: crate::types::ScrollStatus::Draft,
            created_at: Utc::now(),
            updated_at: Utc::now(),
            cleanup_after: None,
        };
        sqlite.create_scroll(&scroll).await.unwrap();

        let ext_ref = ExternalRef {
            system: ExternalSystem::Linear,
            external_id: "LIN-123".to_string(),
            url: "https://linear.app/team/LIN-123".to_string(),
            last_synced: Utc::now().timestamp(),
            sync_direction: crate::sync::types::SyncDirection::Bidirectional,
            token_ref: None,
        };

        bridge
            .link_scroll("link-test-scroll", &ext_ref)
            .await
            .unwrap();

        let retrieved = bridge
            .get_link("link-test-scroll", &ExternalSystem::Linear)
            .await
            .unwrap();

        assert!(retrieved.is_some());
        let r = retrieved.unwrap();
        assert_eq!(r.external_id, "LIN-123");
        assert_eq!(r.system, ExternalSystem::Linear);
    }
}
