use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

use sqlx::SqlitePool;
use tokio::sync::mpsc;
use tracing::{info, warn};

use core_api::{ConfigProperty, ConfigSet, PropertyType};
use core_api::system_bus::{SystemEvent, SystemEventBus};

use crate::chat_hub::ChatHub;
use crate::config::TicConfig;
use crate::config_store::GlobalConfigManager;
use crate::db::mcp_events;
use crate::run_context::{RunContext, RunContextManager};
use crate::session::manager::ChatSessionManager;

const TIC_SOURCE: &str = "tic";
const TIC_AGENT:  &str = "tic";

pub const TIC_ENABLED_KEY:            &str = "tic.enabled";
pub const TIC_SECURITY_GROUP_KEY:     &str = "tic.security_group";
pub const TIC_INTERVAL_MINUTES_KEY:   &str = "tic.interval_minutes";

pub fn config_set() -> ConfigSet {
    ConfigSet {
        name:        "TIC Agent".into(),
        description: "TIC is a background agent that monitors all async events generated by connected MCP servers (new emails, calendar updates, WhatsApp messages, etc.). It reads your notification rules from data/notifications.md and your memory to decide — via an LLM call — which events are worth surfacing. Relevant notifications are forwarded to the home agent set via /sethome.".into(),
        properties:  vec![
            ConfigProperty {
                key:           TIC_ENABLED_KEY.into(),
                name:          "Enabled".into(),
                description:   "Enable or disable the TIC agent. When disabled, no MCP events are processed.".into(),
                property_type: PropertyType::Bool,
                default_value: Some("true".into()),
            },
            ConfigProperty {
                key:           TIC_SECURITY_GROUP_KEY.into(),
                name:          "Security Group".into(),
                description:   "Tool permission group applied to each TIC agent session. Leave empty to use the default group.".into(),
                property_type: PropertyType::SecurityGroup,
                default_value: None,
            },
            ConfigProperty {
                key:           TIC_INTERVAL_MINUTES_KEY.into(),
                name:          "Check Interval (minutes)".into(),
                description:   "How often TIC runs, in minutes. Leave empty to use the value from config.yml (tic.interval_secs).".into(),
                property_type: PropertyType::Int,
                default_value: Some("15".into()),
            },
        ],
    }
}

pub struct TicManager {
    db:                  Arc<SqlitePool>,
    session_mgr:         Arc<ChatSessionManager>,
    hub:                 Arc<ChatHub>,
    config:              TicConfig,
    config_store:        Arc<GlobalConfigManager>,
    run_context_manager: Arc<RunContextManager>,
    system_bus:          Arc<SystemEventBus>,
    /// Guards against concurrent ticks (e.g. if a tick takes longer than the interval).
    running: AtomicBool,
}

impl TicManager {
    pub fn new(
        db:                  Arc<SqlitePool>,
        session_mgr:         Arc<ChatSessionManager>,
        hub:                 Arc<ChatHub>,
        config:              TicConfig,
        config_store:        Arc<GlobalConfigManager>,
        run_context_manager: Arc<RunContextManager>,
        system_bus:          Arc<SystemEventBus>,
    ) -> Arc<Self> {
        Arc::new(Self {
            db,
            session_mgr,
            hub,
            config,
            config_store,
            run_context_manager,
            system_bus,
            running: AtomicBool::new(false),
        })
    }

    /// Force a tick immediately, ignoring the running guard.
    /// Intended for manual triggering (e.g. via the `/api/tic/trigger` endpoint).
    pub async fn tick_now(self: Arc<Self>) {
        if let Err(e) = self.run_tick().await {
            warn!(error = %e, "TicManager: forced tick failed");
        }
    }

    /// Spawn the background timer.
    /// Subscribes to ConfigKeyUpdated so the interval can be changed at runtime.
    pub fn start(self: Arc<Self>, shutdown: tokio_util::sync::CancellationToken) -> tokio::task::JoinHandle<()> {
        tokio::spawn(async move {
            let mut interval_secs = self.effective_interval_secs().await;
            info!("TicManager started (interval={}s, batch={})", interval_secs, self.config.batch_size);

            let mut timer = tokio::time::interval(Duration::from_secs(interval_secs));
            timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

            let mut sys_rx = self.system_bus.subscribe();

            loop {
                tokio::select! {
                    _ = shutdown.cancelled() => {
                        info!("TicManager: stopping");
                        break;
                    }
                    res = sys_rx.recv() => {
                        if let Ok(SystemEvent::ConfigKeyUpdated { key, new_value, .. }) = res {
                            if key == TIC_INTERVAL_MINUTES_KEY {
                                if let Ok(mins) = new_value.parse::<u64>() {
                                    let new_secs = mins.max(1) * 60;
                                    if new_secs != interval_secs {
                                        interval_secs = new_secs;
                                        timer = tokio::time::interval(Duration::from_secs(interval_secs));
                                        timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
                                        info!(secs = interval_secs, "TicManager: interval updated");
                                    }
                                }
                            }
                        }
                    }
                    _ = timer.tick() => {
                        self.tick().await;
                    }
                }
            }
        })
    }

    async fn effective_interval_secs(&self) -> u64 {
        if let Ok(Some(val)) = self.config_store.get(TIC_INTERVAL_MINUTES_KEY).await {
            if let Ok(mins) = val.parse::<u64>() {
                if mins > 0 {
                    return mins * 60;
                }
            }
        }
        self.config.interval_secs
    }

    async fn is_enabled(&self) -> bool {
        match self.config_store.get(TIC_ENABLED_KEY).await {
            Ok(Some(v)) => v != "false",
            _           => true,
        }
    }

    async fn tick(&self) {
        if !self.is_enabled().await {
            return;
        }
        // Prevent concurrent ticks.
        if self.running.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
            warn!("TicManager: previous tick still running, skipping");
            return;
        }

        let result = self.run_tick().await;
        self.running.store(false, Ordering::SeqCst);

        if let Err(e) = result {
            warn!(error = %e, "TicManager: tick failed");
        }
    }

    async fn run_tick(&self) -> anyhow::Result<()> {
        // 1. Fetch the oldest N unprocessed events.
        let events = mcp_events::pending_limited(&self.db, self.config.batch_size).await?;
        if events.is_empty() {
            return Ok(());
        }

        info!(count = events.len(), "TicManager: processing event batch");

        // 2. Mark as processed BEFORE running the agent — avoids double-processing
        //    if the process crashes mid-turn.
        let ids: Vec<i64> = events.iter().map(|e| e.id).collect();
        mcp_events::mark_processed(&self.db, &ids).await?;

        // 3. Serialize events into the agent prompt.
        let prompt = build_prompt(&events);

        // 4. Create a fresh ephemeral session (agent_id = "tic", source = "tic").
        //    We bypass ChatHub entirely — TIC is not a user-facing source and should
        //    not appear in the sources table or consume a broadcast channel.
        let (session_id, _) = self.session_mgr.create_session(TIC_AGENT, TIC_SOURCE, false, true, None).await?;
        let handler = self.session_mgr.get_or_create_handler(session_id).await?;
        handler.set_auto_deny_approvals();

        // 5. Apply run context if configured in DB.
        if let Ok(Some(rc_id)) = self.config_store.get(TIC_SECURITY_GROUP_KEY).await {
            if !rc_id.is_empty() {
                let rc = RunContext::with_security_group(Some(rc_id.clone()));
                if let Err(e) = self.run_context_manager.set_session_run_context(session_id, Some(&rc)).await {
                    warn!(error = %e, rc_id, "TicManager: failed to set run context");
                }
            }
        }

        // 6. Sink for session events — nobody subscribes; drop the receiver immediately
        //    so the channel is drained without buffering.
        let (tx, _rx) = mpsc::channel(32);
        let notify = crate::tools::notify::make_tool(Arc::clone(&self.hub), "TIC");

        handler.handle_message(&prompt, None, None, None, None, vec![notify], std::collections::HashMap::new(), tx, true, None, None).await?;

        info!(session_id, count = events.len(), "TicManager: tick complete");
        Ok(())
    }
}

// ── Prompt builder ─────────────────────────────────────────────────────────────

fn build_prompt(events: &[crate::db::mcp_events::McpEvent]) -> String {
    use std::fmt::Write;

    let n = events.len();
    let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
    let mut out = format!("[TIC] {n} pending event(s) — {now}\n");

    for (i, ev) in events.iter().enumerate() {
        let _ = write!(
            out,
            "\n=== Event {}/{n} ===\nSource:   {}\nType:     {}\nReceived: {}\nPayload:\n{}\n",
            i + 1,
            ev.source,
            ev.method,
            ev.created_at,
            indent_payload(&ev.payload),
        );
    }

    out
}

/// Pretty-print a JSON payload with 2-space indent, falling back to raw string.
fn indent_payload(payload: &str) -> String {
    if let Ok(v) = serde_json::from_str::<serde_json::Value>(payload) {
        if let Ok(pretty) = serde_json::to_string_pretty(&v) {
            return pretty.lines().map(|l| format!("  {l}")).collect::<Vec<_>>().join("\n");
        }
    }
    format!("  {payload}")
}
