use crate::models::graylog::{GraylogCredentials, GraylogDetection, GraylogSubject};
use crate::models::module::{ProjectModule, MODULE_GRAYLOG_AUTO_RESOLVE};
use crate::models::ticket::ProcessedTicket;
use crate::services::crypto;
use crate::services::graylog_client::GraylogClient;
use crate::services::graylog_scoring::{group_subjects, SubjectAggregate};
use rusqlite::Connection;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Emitter};
use tokio::time::{interval, Duration};

fn is_ticket_active(status: &str) -> bool {
    matches!(status, "Pending" | "Analyzing" | "Developing")
}

fn should_trigger_subject(score: i32, threshold: i32, has_active_ticket: bool) -> bool {
    score >= threshold && !has_active_ticket
}

fn synthetic_artifact_id(subject_key: &str) -> i32 {
    // Stable FNV-1a hash so the synthetic id remains deterministic across runs.
    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
    const FNV_PRIME: u64 = 0x100000001b3;

    let mut hash = FNV_OFFSET;
    for byte in subject_key.as_bytes() {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(FNV_PRIME);
    }

    let raw = (hash & 0x3fff_ffff) as i32;
    -raw.max(1)
}

fn subject_payload(aggregate: &SubjectAggregate) -> String {
    serde_json::json!({
        "source": aggregate.source,
        "normalized_message": aggregate.normalized_message,
        "counts": {
            "critical": aggregate.counts.critical,
            "error": aggregate.counts.error,
            "warning": aggregate.counts.warning,
            "total": aggregate.total_count,
        },
        "last_seen_at": aggregate.last_seen_at,
        "score": aggregate.score,
        "samples": aggregate.sample_events,
    })
    .to_string()
}

fn emit_polling_error(app_handle: &AppHandle, project_id: &str, error: &str) {
    let _ = app_handle.emit(
        "graylog-polling-error",
        serde_json::json!({
            "project_id": project_id,
            "error": error,
        }),
    );
}

pub fn start(
    db: Arc<Mutex<Connection>>,
    encryption_key: [u8; 32],
    http_client: reqwest::Client,
    app_handle: AppHandle,
) {
    tauri::async_runtime::spawn(async move {
        let mut tick = interval(Duration::from_secs(60));
        let mut last_polled_at: HashMap<String, chrono::DateTime<chrono::Utc>> = HashMap::new();

        loop {
            tick.tick().await;

            let credentials = {
                let conn = match db.lock() {
                    Ok(conn) => conn,
                    Err(err) => {
                        eprintln!("graylog_poller: failed to lock db: {}", err);
                        continue;
                    }
                };

                match GraylogCredentials::list_all(&conn) {
                    Ok(values) => values,
                    Err(err) => {
                        eprintln!("graylog_poller: failed to load credentials: {}", err);
                        continue;
                    }
                }
            };

            let now = chrono::Utc::now();
            let known_projects: HashSet<String> =
                credentials.iter().map(|item| item.project_id.clone()).collect();
            last_polled_at.retain(|project_id, _| known_projects.contains(project_id));

            for credential in credentials {
                let project_id = credential.project_id.clone();
                let polling_interval_minutes = credential.polling_interval_minutes.max(1) as i64;

                let due = match last_polled_at.get(&project_id) {
                    Some(last_at) => {
                        now.signed_duration_since(*last_at).num_minutes() >= polling_interval_minutes
                    }
                    None => true,
                };

                if !due {
                    continue;
                }

                if let Err(err) = poll_project_once(
                    &db,
                    &encryption_key,
                    &http_client,
                    &app_handle,
                    &project_id,
                )
                .await
                {
                    eprintln!("graylog_poller: project {} failed: {}", project_id, err);
                    emit_polling_error(&app_handle, &project_id, &err);
                }

                last_polled_at.insert(project_id, now);
            }
        }
    });
}

pub async fn poll_project_once(
    db: &Arc<Mutex<Connection>>,
    encryption_key: &[u8; 32],
    http_client: &reqwest::Client,
    app_handle: &AppHandle,
    project_id: &str,
) -> Result<i32, String> {
    let credentials = {
        let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;

        let module_enabled = ProjectModule::is_enabled(&conn, project_id, MODULE_GRAYLOG_AUTO_RESOLVE)
            .map_err(|e| format!("module lookup failed: {}", e))?;

        if !module_enabled {
            return Ok(0);
        }

        GraylogCredentials::get_by_project(&conn, project_id)
            .map_err(|e| format!("credentials lookup failed: {}", e))?
            .ok_or_else(|| "No Graylog credentials configured".to_string())?
    };

    let token = crypto::decrypt(encryption_key, &credentials.api_token_encrypted)
        .map_err(|e| format!("token decrypt failed: {}", e))?;

    let client = GraylogClient::new(http_client, &credentials.base_url, &token);
    let query = if credentials.query_filter.trim().is_empty() {
        "level:(critical OR error OR warning)".to_string()
    } else {
        credentials.query_filter.clone()
    };
    let range_seconds = credentials.lookback_minutes.max(1) * 60;

    let _ = app_handle.emit(
        "graylog-polling-started",
        serde_json::json!({ "project_id": project_id }),
    );

    let events = match client
        .search_relative(&query, credentials.stream_id.as_deref(), range_seconds)
        .await
    {
        Ok(events) => events,
        Err(err) => {
            emit_polling_error(app_handle, project_id, &err);
            return Err(err);
        }
    };

    let now = chrono::Utc::now();
    let window_start = (now - chrono::Duration::minutes(credentials.lookback_minutes as i64))
        .to_rfc3339();
    let window_end = now.to_rfc3339();

    let aggregates = group_subjects(&events, now);
    let mut triggered_count = 0i32;

    for aggregate in aggregates {
        let (subject, has_active_ticket) = {
            let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;

            let mut subject = GraylogSubject::upsert_subject(
                &conn,
                project_id,
                &aggregate.subject_key,
                &aggregate.source,
                &aggregate.normalized_message,
                &aggregate.last_seen_at,
                aggregate.score,
            )
            .map_err(|e| format!("upsert subject failed: {}", e))?;

            let mut has_active_ticket = false;
            if let Some(active_ticket_id) = subject.active_ticket_id.clone() {
                match ProcessedTicket::get_by_id(&conn, &active_ticket_id) {
                    Ok(ticket) if is_ticket_active(&ticket.status) => {
                        has_active_ticket = true;
                    }
                    _ => {
                        GraylogSubject::set_active_ticket(&conn, &subject.id, None, "idle")
                            .map_err(|e| format!("clear stale subject ticket failed: {}", e))?;
                        subject.active_ticket_id = None;
                        subject.status = "idle".to_string();
                    }
                }
            }

            (subject, has_active_ticket)
        };

        let should_trigger = should_trigger_subject(
            aggregate.score,
            credentials.score_threshold,
            has_active_ticket,
        );

        let triggered_ticket_id = if should_trigger {
            let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
            let ticket = ProcessedTicket::insert_external(
                &conn,
                project_id,
                "graylog",
                Some(&subject.id),
                synthetic_artifact_id(&aggregate.subject_key),
                &format!(
                    "[Graylog] {} - {}",
                    aggregate.source, aggregate.normalized_message
                ),
                &subject_payload(&aggregate),
            )
            .map_err(|e| format!("insert graylog ticket failed: {}", e))?;

            GraylogSubject::set_active_ticket(&conn, &subject.id, Some(&ticket.id), "queued")
                .map_err(|e| format!("set active ticket failed: {}", e))?;

            triggered_count += 1;
            let _ = app_handle.emit(
                "graylog-subject-triggered",
                serde_json::json!({
                    "project_id": project_id,
                    "subject_id": subject.id,
                    "ticket_id": ticket.id,
                    "score": aggregate.score,
                }),
            );

            Some(ticket.id)
        } else {
            None
        };

        {
            let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
            GraylogDetection::insert(
                &conn,
                &subject.id,
                &window_start,
                &window_end,
                aggregate.counts.critical,
                aggregate.counts.error,
                aggregate.counts.warning,
                aggregate.total_count,
                &aggregate.last_seen_at,
                aggregate.score,
                should_trigger,
                triggered_ticket_id.as_deref(),
            )
            .map_err(|e| format!("insert detection failed: {}", e))?;
        }
    }

    let _ = app_handle.emit(
        "graylog-polling-finished",
        serde_json::json!({
            "project_id": project_id,
            "triggered_count": triggered_count,
        }),
    );

    Ok(triggered_count)
}

#[cfg(test)]
mod tests {
    use super::should_trigger_subject;

    #[test]
    fn test_should_trigger_subject_respects_active_ticket() {
        assert!(should_trigger_subject(82, 70, false));
        assert!(!should_trigger_subject(82, 70, true));
        assert!(!should_trigger_subject(60, 70, false));
    }
}
