// Clipboard history module for tracking and managing copied code snippets
// Implements issue #25 - Clipboard History feature

use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use tauri_plugin_store::StoreExt;
use uuid::Uuid;

const STORE_FILE: &str = "hikari-clipboard.json";
const HISTORY_KEY: &str = "clipboard_history";
const MAX_HISTORY_SIZE: usize = 100;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipboardEntry {
    pub id: String,
    pub content: String,
    pub language: Option<String>,
    pub source: Option<String>,
    pub timestamp: String,
    pub is_pinned: bool,
}

impl ClipboardEntry {
    pub fn new(content: String, language: Option<String>, source: Option<String>) -> Self {
        Self {
            id: Uuid::new_v4().to_string(),
            content,
            language,
            source,
            timestamp: chrono::Utc::now().to_rfc3339(),
            is_pinned: false,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct ClipboardHistory {
    entries: Vec<ClipboardEntry>,
}

// Track last clipboard content to avoid duplicates
#[derive(Default)]
struct ClipboardState {
    last_content: Option<String>,
}

static CLIPBOARD_STATE: Mutex<ClipboardState> = Mutex::new(ClipboardState { last_content: None });

fn load_history(app: &tauri::AppHandle) -> ClipboardHistory {
    let store = app.store(STORE_FILE).ok();
    store
        .and_then(|s| s.get(HISTORY_KEY))
        .and_then(|v| serde_json::from_value(v.clone()).ok())
        .unwrap_or_default()
}

fn save_history(app: &tauri::AppHandle, history: &ClipboardHistory) -> Result<(), String> {
    let store = app.store(STORE_FILE).map_err(|e| e.to_string())?;
    store.set(
        HISTORY_KEY,
        serde_json::to_value(history).map_err(|e| e.to_string())?,
    );
    store.save().map_err(|e| e.to_string())?;
    Ok(())
}

/// List all clipboard entries, optionally filtered by language
#[tauri::command]
pub fn list_clipboard_entries(
    app: tauri::AppHandle,
    language: Option<String>,
) -> Result<Vec<ClipboardEntry>, String> {
    let history = load_history(&app);
    let entries = if let Some(lang) = language {
        history
            .entries
            .into_iter()
            .filter(|e| e.language.as_ref() == Some(&lang))
            .collect()
    } else {
        history.entries
    };
    Ok(entries)
}

/// Capture current clipboard content and add to history
#[tauri::command]
pub fn capture_clipboard(
    app: tauri::AppHandle,
    content: String,
    language: Option<String>,
    source: Option<String>,
) -> Result<ClipboardEntry, String> {
    // Check for duplicate (same content as last capture)
    {
        let mut state = CLIPBOARD_STATE.lock().map_err(|e| e.to_string())?;
        if state.last_content.as_ref() == Some(&content) {
            // Return existing entry if content is the same
            let history = load_history(&app);
            if let Some(entry) = history.entries.first() {
                if entry.content == content {
                    return Ok(entry.clone());
                }
            }
        }
        state.last_content = Some(content.clone());
    }

    let entry = ClipboardEntry::new(content, language, source);
    let mut history = load_history(&app);

    // Add to front of history
    history.entries.insert(0, entry.clone());

    // Enforce max size (keep pinned entries)
    let mut pinned: Vec<ClipboardEntry> = history
        .entries
        .iter()
        .filter(|e| e.is_pinned)
        .cloned()
        .collect();
    let mut unpinned: Vec<ClipboardEntry> = history
        .entries
        .into_iter()
        .filter(|e| !e.is_pinned)
        .collect();

    // Trim unpinned entries if over max size
    if unpinned.len() + pinned.len() > MAX_HISTORY_SIZE {
        let max_unpinned = MAX_HISTORY_SIZE.saturating_sub(pinned.len());
        unpinned.truncate(max_unpinned);
    }

    // Merge back, pinned first then unpinned
    pinned.extend(unpinned);
    history.entries = pinned;

    // Sort by timestamp descending (newest first), pinned entries stay at top
    history.entries.sort_by(|a, b| {
        if a.is_pinned && !b.is_pinned {
            std::cmp::Ordering::Less
        } else if !a.is_pinned && b.is_pinned {
            std::cmp::Ordering::Greater
        } else {
            b.timestamp.cmp(&a.timestamp)
        }
    });

    save_history(&app, &history)?;
    Ok(entry)
}

/// Delete a clipboard entry by ID
#[tauri::command]
pub fn delete_clipboard_entry(app: tauri::AppHandle, id: String) -> Result<(), String> {
    let mut history = load_history(&app);
    history.entries.retain(|e| e.id != id);
    save_history(&app, &history)?;
    Ok(())
}

/// Toggle pin status of an entry
#[tauri::command]
pub fn toggle_pin_clipboard_entry(
    app: tauri::AppHandle,
    id: String,
) -> Result<ClipboardEntry, String> {
    let mut history = load_history(&app);
    let entry = history
        .entries
        .iter_mut()
        .find(|e| e.id == id)
        .ok_or("Entry not found")?;

    entry.is_pinned = !entry.is_pinned;
    let updated_entry = entry.clone();

    // Re-sort to move pinned entries to top
    history.entries.sort_by(|a, b| {
        if a.is_pinned && !b.is_pinned {
            std::cmp::Ordering::Less
        } else if !a.is_pinned && b.is_pinned {
            std::cmp::Ordering::Greater
        } else {
            b.timestamp.cmp(&a.timestamp)
        }
    });

    save_history(&app, &history)?;
    Ok(updated_entry)
}

/// Clear all non-pinned entries
#[tauri::command]
pub fn clear_clipboard_history(app: tauri::AppHandle) -> Result<(), String> {
    let mut history = load_history(&app);
    history.entries.retain(|e| e.is_pinned);
    save_history(&app, &history)?;
    Ok(())
}

/// Search clipboard entries by content
#[tauri::command]
pub fn search_clipboard_entries(
    app: tauri::AppHandle,
    query: String,
) -> Result<Vec<ClipboardEntry>, String> {
    let history = load_history(&app);
    let query_lower = query.to_lowercase();
    let entries = history
        .entries
        .into_iter()
        .filter(|e| {
            e.content.to_lowercase().contains(&query_lower)
                || e.language
                    .as_ref()
                    .is_some_and(|l| l.to_lowercase().contains(&query_lower))
                || e.source
                    .as_ref()
                    .is_some_and(|s| s.to_lowercase().contains(&query_lower))
        })
        .collect();
    Ok(entries)
}

/// Get all unique languages from history
#[tauri::command]
pub fn get_clipboard_languages(app: tauri::AppHandle) -> Result<Vec<String>, String> {
    let history = load_history(&app);
    let mut languages: Vec<String> = history
        .entries
        .iter()
        .filter_map(|e| e.language.clone())
        .collect();
    languages.sort();
    languages.dedup();
    Ok(languages)
}

/// Update the language of an entry
#[tauri::command]
pub fn update_clipboard_language(
    app: tauri::AppHandle,
    id: String,
    language: Option<String>,
) -> Result<ClipboardEntry, String> {
    let mut history = load_history(&app);
    let entry = history
        .entries
        .iter_mut()
        .find(|e| e.id == id)
        .ok_or("Entry not found")?;

    entry.language = language;
    let updated_entry = entry.clone();

    save_history(&app, &history)?;
    Ok(updated_entry)
}

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

    // ==================== ClipboardEntry tests ====================

    #[test]
    fn test_clipboard_entry_new() {
        let entry = ClipboardEntry::new(
            "let x = 42;".to_string(),
            Some("rust".to_string()),
            Some("main.rs".to_string()),
        );

        assert_eq!(entry.content, "let x = 42;");
        assert_eq!(entry.language, Some("rust".to_string()));
        assert_eq!(entry.source, Some("main.rs".to_string()));
        assert!(!entry.is_pinned);
        assert!(!entry.id.is_empty());
        assert!(!entry.timestamp.is_empty());
    }

    #[test]
    fn test_clipboard_entry_new_without_optional_fields() {
        let entry = ClipboardEntry::new("some content".to_string(), None, None);

        assert_eq!(entry.content, "some content");
        assert!(entry.language.is_none());
        assert!(entry.source.is_none());
        assert!(!entry.is_pinned);
    }

    #[test]
    fn test_clipboard_entry_unique_ids() {
        let entry1 = ClipboardEntry::new("content1".to_string(), None, None);
        let entry2 = ClipboardEntry::new("content2".to_string(), None, None);

        assert_ne!(entry1.id, entry2.id);
    }

    #[test]
    fn test_clipboard_entry_serialization() {
        let entry = ClipboardEntry::new(
            "fn main() {}".to_string(),
            Some("rust".to_string()),
            Some("lib.rs".to_string()),
        );

        let json = serde_json::to_string(&entry).unwrap();
        assert!(json.contains("fn main() {}"));
        assert!(json.contains("rust"));
        assert!(json.contains("lib.rs"));
        assert!(json.contains("is_pinned"));

        let deserialized: ClipboardEntry = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.content, entry.content);
        assert_eq!(deserialized.language, entry.language);
        assert_eq!(deserialized.source, entry.source);
        assert_eq!(deserialized.id, entry.id);
    }

    #[test]
    fn test_clipboard_entry_clone() {
        let entry = ClipboardEntry::new(
            "original".to_string(),
            Some("python".to_string()),
            None,
        );

        let cloned = entry.clone();
        assert_eq!(cloned.content, entry.content);
        assert_eq!(cloned.id, entry.id);
        assert_eq!(cloned.language, entry.language);
    }

    #[test]
    fn test_clipboard_entry_timestamp_is_rfc3339() {
        let entry = ClipboardEntry::new("test".to_string(), None, None);

        // RFC3339 timestamp should parse successfully
        let parsed = chrono::DateTime::parse_from_rfc3339(&entry.timestamp);
        assert!(parsed.is_ok());
    }

    // ==================== ClipboardHistory tests ====================

    #[test]
    fn test_clipboard_history_default() {
        let history = ClipboardHistory::default();
        assert!(history.entries.is_empty());
    }

    #[test]
    fn test_clipboard_history_serialization() {
        let mut history = ClipboardHistory::default();
        history.entries.push(ClipboardEntry::new(
            "entry1".to_string(),
            Some("js".to_string()),
            None,
        ));
        history.entries.push(ClipboardEntry::new(
            "entry2".to_string(),
            None,
            Some("file.txt".to_string()),
        ));

        let json = serde_json::to_string(&history).unwrap();
        assert!(json.contains("entry1"));
        assert!(json.contains("entry2"));
        assert!(json.contains("js"));
        assert!(json.contains("file.txt"));

        let deserialized: ClipboardHistory = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.entries.len(), 2);
    }

    #[test]
    fn test_clipboard_history_entries_order() {
        let mut history = ClipboardHistory::default();

        history.entries.push(ClipboardEntry::new("first".to_string(), None, None));
        history.entries.push(ClipboardEntry::new("second".to_string(), None, None));
        history.entries.push(ClipboardEntry::new("third".to_string(), None, None));

        assert_eq!(history.entries[0].content, "first");
        assert_eq!(history.entries[1].content, "second");
        assert_eq!(history.entries[2].content, "third");
    }

    // ==================== ClipboardState tests ====================

    #[test]
    fn test_clipboard_state_default() {
        let state = ClipboardState::default();
        assert!(state.last_content.is_none());
    }

    #[test]
    fn test_clipboard_state_with_content() {
        let state = ClipboardState {
            last_content: Some("cached content".to_string()),
        };
        assert_eq!(state.last_content, Some("cached content".to_string()));
    }

    // ==================== MAX_HISTORY_SIZE constant test ====================

    #[test]
    fn test_max_history_size_is_reasonable() {
        assert_eq!(MAX_HISTORY_SIZE, 100);
        // Compile-time assertions for constant bounds
        const _: () = assert!(MAX_HISTORY_SIZE > 0);
        const _: () = assert!(MAX_HISTORY_SIZE <= 1000); // Sanity check
    }

    // ==================== Pinned entry sorting tests ====================

    #[test]
    #[allow(clippy::useless_vec)]
    fn test_pinned_entries_sorting() {
        let mut entries = vec![
            ClipboardEntry {
                id: "1".to_string(),
                content: "unpinned older".to_string(),
                language: None,
                source: None,
                timestamp: "2024-01-01T00:00:00Z".to_string(),
                is_pinned: false,
            },
            ClipboardEntry {
                id: "2".to_string(),
                content: "pinned".to_string(),
                language: None,
                source: None,
                timestamp: "2024-01-02T00:00:00Z".to_string(),
                is_pinned: true,
            },
            ClipboardEntry {
                id: "3".to_string(),
                content: "unpinned newer".to_string(),
                language: None,
                source: None,
                timestamp: "2024-01-03T00:00:00Z".to_string(),
                is_pinned: false,
            },
        ];

        // Apply the same sorting logic as used in the module
        entries.sort_by(|a, b| {
            if a.is_pinned && !b.is_pinned {
                std::cmp::Ordering::Less
            } else if !a.is_pinned && b.is_pinned {
                std::cmp::Ordering::Greater
            } else {
                b.timestamp.cmp(&a.timestamp)
            }
        });

        // Pinned should be first
        assert!(entries[0].is_pinned);
        assert_eq!(entries[0].id, "2");

        // Then unpinned sorted by timestamp descending (newest first)
        assert_eq!(entries[1].id, "3"); // newer unpinned
        assert_eq!(entries[2].id, "1"); // older unpinned
    }

    #[test]
    #[allow(clippy::useless_vec)]
    fn test_multiple_pinned_entries_sorting() {
        let mut entries = vec![
            ClipboardEntry {
                id: "1".to_string(),
                content: "pinned older".to_string(),
                language: None,
                source: None,
                timestamp: "2024-01-01T00:00:00Z".to_string(),
                is_pinned: true,
            },
            ClipboardEntry {
                id: "2".to_string(),
                content: "unpinned".to_string(),
                language: None,
                source: None,
                timestamp: "2024-01-02T00:00:00Z".to_string(),
                is_pinned: false,
            },
            ClipboardEntry {
                id: "3".to_string(),
                content: "pinned newer".to_string(),
                language: None,
                source: None,
                timestamp: "2024-01-03T00:00:00Z".to_string(),
                is_pinned: true,
            },
        ];

        entries.sort_by(|a, b| {
            if a.is_pinned && !b.is_pinned {
                std::cmp::Ordering::Less
            } else if !a.is_pinned && b.is_pinned {
                std::cmp::Ordering::Greater
            } else {
                b.timestamp.cmp(&a.timestamp)
            }
        });

        // Both pinned first, sorted by timestamp
        assert!(entries[0].is_pinned);
        assert_eq!(entries[0].id, "3"); // pinned newer
        assert!(entries[1].is_pinned);
        assert_eq!(entries[1].id, "1"); // pinned older
        // Then unpinned
        assert!(!entries[2].is_pinned);
        assert_eq!(entries[2].id, "2");
    }

    // ==================== Entry filtering tests ====================

    #[test]
    fn test_filter_entries_by_language() {
        let history = ClipboardHistory {
            entries: vec![
                ClipboardEntry {
                    id: "1".to_string(),
                    content: "rust code".to_string(),
                    language: Some("rust".to_string()),
                    source: None,
                    timestamp: "2024-01-01T00:00:00Z".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "2".to_string(),
                    content: "js code".to_string(),
                    language: Some("javascript".to_string()),
                    source: None,
                    timestamp: "2024-01-02T00:00:00Z".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "3".to_string(),
                    content: "more rust".to_string(),
                    language: Some("rust".to_string()),
                    source: None,
                    timestamp: "2024-01-03T00:00:00Z".to_string(),
                    is_pinned: false,
                },
            ],
        };

        let filtered: Vec<_> = history
            .entries
            .iter()
            .filter(|e| e.language.as_ref() == Some(&"rust".to_string()))
            .collect();

        assert_eq!(filtered.len(), 2);
        assert!(filtered.iter().all(|e| e.language == Some("rust".to_string())));
    }

    #[test]
    fn test_search_entries_by_content() {
        let history = ClipboardHistory {
            entries: vec![
                ClipboardEntry {
                    id: "1".to_string(),
                    content: "fn hello_world()".to_string(),
                    language: Some("rust".to_string()),
                    source: None,
                    timestamp: "2024-01-01T00:00:00Z".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "2".to_string(),
                    content: "function hello()".to_string(),
                    language: Some("javascript".to_string()),
                    source: None,
                    timestamp: "2024-01-02T00:00:00Z".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "3".to_string(),
                    content: "def goodbye()".to_string(),
                    language: Some("python".to_string()),
                    source: None,
                    timestamp: "2024-01-03T00:00:00Z".to_string(),
                    is_pinned: false,
                },
            ],
        };

        let query = "hello";
        let query_lower = query.to_lowercase();
        let filtered: Vec<_> = history
            .entries
            .iter()
            .filter(|e| e.content.to_lowercase().contains(&query_lower))
            .collect();

        assert_eq!(filtered.len(), 2);
        assert!(filtered[0].content.contains("hello"));
        assert!(filtered[1].content.contains("hello"));
    }

    #[test]
    fn test_search_entries_case_insensitive() {
        let history = ClipboardHistory {
            entries: vec![
                ClipboardEntry {
                    id: "1".to_string(),
                    content: "HELLO WORLD".to_string(),
                    language: None,
                    source: None,
                    timestamp: "2024-01-01T00:00:00Z".to_string(),
                    is_pinned: false,
                },
            ],
        };

        let query = "hello";
        let query_lower = query.to_lowercase();
        let filtered: Vec<_> = history
            .entries
            .iter()
            .filter(|e| e.content.to_lowercase().contains(&query_lower))
            .collect();

        assert_eq!(filtered.len(), 1);
    }

    // ==================== Unique languages extraction test ====================

    #[test]
    fn test_extract_unique_languages() {
        let history = ClipboardHistory {
            entries: vec![
                ClipboardEntry {
                    id: "1".to_string(),
                    content: "".to_string(),
                    language: Some("rust".to_string()),
                    source: None,
                    timestamp: "".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "2".to_string(),
                    content: "".to_string(),
                    language: Some("javascript".to_string()),
                    source: None,
                    timestamp: "".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "3".to_string(),
                    content: "".to_string(),
                    language: Some("rust".to_string()), // Duplicate
                    source: None,
                    timestamp: "".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "4".to_string(),
                    content: "".to_string(),
                    language: None, // No language
                    source: None,
                    timestamp: "".to_string(),
                    is_pinned: false,
                },
            ],
        };

        let mut languages: Vec<String> = history
            .entries
            .iter()
            .filter_map(|e| e.language.clone())
            .collect();
        languages.sort();
        languages.dedup();

        assert_eq!(languages.len(), 2);
        assert!(languages.contains(&"rust".to_string()));
        assert!(languages.contains(&"javascript".to_string()));
    }

    // ==================== Retain pinned entries test ====================

    #[test]
    fn test_retain_pinned_on_clear() {
        let mut history = ClipboardHistory {
            entries: vec![
                ClipboardEntry {
                    id: "1".to_string(),
                    content: "pinned".to_string(),
                    language: None,
                    source: None,
                    timestamp: "".to_string(),
                    is_pinned: true,
                },
                ClipboardEntry {
                    id: "2".to_string(),
                    content: "unpinned".to_string(),
                    language: None,
                    source: None,
                    timestamp: "".to_string(),
                    is_pinned: false,
                },
                ClipboardEntry {
                    id: "3".to_string(),
                    content: "another pinned".to_string(),
                    language: None,
                    source: None,
                    timestamp: "".to_string(),
                    is_pinned: true,
                },
            ],
        };

        // Simulate clear (keep only pinned)
        history.entries.retain(|e| e.is_pinned);

        assert_eq!(history.entries.len(), 2);
        assert!(history.entries.iter().all(|e| e.is_pinned));
    }
}
