use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use url::Url;

use super::CrawlerError;
use crate::Info;

/// Serializable crawl state used to resume discovery and extraction.
#[derive(Debug, Serialize, Deserialize)]
#[serde(bound = "T: Info + Clone")]
pub struct Checkpoint<T: Info + Clone> {
    /// Original crawl seed URL.
    pub start_url: Url,
    /// URLs visited during the discovery phase.
    pub visited_urls: HashSet<Url>,
    /// Discovered target URLs to process.
    pub target_urls: HashSet<Url>,
    /// Text from the page where each target was discovered. Keyed by target key.
    #[serde(default)]
    pub source_texts: HashMap<String, String>,
    /// Page texts per target key.
    pub target_page_texts: HashMap<String, Vec<String>>,
    /// Extracted results per target key.
    pub results: Vec<(String, T)>,
    /// RFC3339 timestamp for when this checkpoint was created.
    pub timestamp: String,
}

impl<T: Info + Clone> Checkpoint<T> {
    /// Create an empty checkpoint for `start_url`.
    pub fn new(start_url: Url) -> Self {
        Self {
            start_url,
            visited_urls: HashSet::new(),
            target_urls: HashSet::new(),
            source_texts: HashMap::new(),
            target_page_texts: HashMap::new(),
            results: Vec::new(),
            timestamp: chrono::Utc::now().to_rfc3339(),
        }
    }

    /// Persist this checkpoint as CBOR.
    pub fn save(&self, path: &Path) -> Result<(), CrawlerError> {
        let mut buf = Vec::new();
        ciborium::into_writer(self, &mut buf)
            .map_err(|e| CrawlerError::FileError(std::io::Error::other(e.to_string())))?;
        fs::write(path, buf)?;
        log::debug!("💾 Checkpoint saved to: {}", path.display());
        Ok(())
    }

    /// Load a CBOR checkpoint from disk.
    pub fn load(path: &Path) -> Result<Self, CrawlerError> {
        let buf = fs::read(path)?;
        let checkpoint = ciborium::from_reader(buf.as_slice())
            .map_err(|e| CrawlerError::FileError(std::io::Error::other(e.to_string())))?;
        log::info!("📂 Checkpoint loaded from: {}", path.display());
        Ok(checkpoint)
    }

    /// Mark a URL as visited or queued.
    pub fn add_visited(&mut self, url: Url) {
        self.visited_urls.insert(url);
    }

    /// Add discovered target URLs.
    pub fn add_target_urls(&mut self, urls: &HashSet<Url>) {
        self.target_urls.extend(urls.iter().cloned());
    }

    /// Store the discovery text for a target key. If text already exists,
    /// the new text is appended.
    pub fn add_source_text(&mut self, key: String, text: String) {
        self.source_texts
            .entry(key)
            .and_modify(|existing| {
                existing.push('\n');
                existing.push_str(&text);
            })
            .or_insert(text);
    }

    /// Return source listing text for a target key.
    pub fn get_source_text(&self, key: &str) -> Option<&String> {
        self.source_texts.get(key)
    }

    /// Store fetched page text for a target key.
    pub fn add_target_page_texts(&mut self, key: String, texts: Vec<String>) {
        self.target_page_texts.insert(key, texts);
    }

    /// Return fetched page text for a target key.
    pub fn get_target_page_texts(&self, key: &str) -> Option<&Vec<String>> {
        self.target_page_texts.get(key)
    }

    /// Insert or overwrite the extracted result for `key`.
    pub fn add_result(&mut self, key: String, info: T) {
        if let Some(existing) = self.results.iter_mut().find(|(k, _)| k == &key) {
            existing.1 = info;
        } else {
            self.results.push((key, info));
        }
    }

    /// Build a human-readable checkpoint filename for `start_url`.
    pub fn checkpoint_filename(start_url: &Url) -> String {
        let domain = start_url.host_str().unwrap_or("unknown");
        let timestamp = chrono::Local::now().format("%Y_%m_%d_%H_%M_%S");
        format!("checkpoint_{}_{}", domain, timestamp)
    }
}
