use crate::Info;
use log::info;
use std::{
    collections::HashSet,
    fs,
    path::{Path, PathBuf},
};
use url::Url;

use super::GuiError;

/// Scan `dir` for checkpoint files (no extension, filename starts with "checkpoint_").
pub fn scan_checkpoints(dir: &str) -> Vec<PathBuf> {
    let dir = Path::new(dir);
    let Ok(entries) = fs::read_dir(dir) else {
        return Vec::new();
    };
    let mut checkpoints: Vec<PathBuf> = entries
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| {
            p.is_file()
                && p.file_name()
                    .and_then(|n| n.to_str())
                    .map(|n| n.starts_with("checkpoint_"))
                    .unwrap_or(false)
        })
        .collect();
    checkpoints.sort_unstable_by(|a, b| b.cmp(a)); // newest first
    checkpoints
}

/// Render a checkpoint filename as `{host} YYYY-MM-DD HH:MM:SS` when it
/// matches the standard `checkpoint_{host}_{YYYY}_{MM}_{DD}_{HH}_{MM}_{SS}`
/// pattern. Falls back to the raw filename when parsing fails.
pub fn checkpoint_display_name(path: &Path) -> String {
    let Some(name) = path.file_name().and_then(|file| file.to_str()) else {
        return path.display().to_string();
    };
    let Some(rest) = name.strip_prefix("checkpoint_") else {
        return name.to_string();
    };

    let parts: Vec<_> = rest.split('_').collect();
    if parts.len() < 7 {
        return name.to_string();
    }

    let (domain_parts, timestamp_parts) = parts.split_at(parts.len() - 6);
    let [year, month, day, hour, minute, second] = timestamp_parts else {
        return name.to_string();
    };
    let domain = domain_parts.join("_");
    if domain.is_empty()
        || [year, month, day, hour, minute, second]
            .iter()
            .any(|part| !part.chars().all(|c| c.is_ascii_digit()))
    {
        return name.to_string();
    }

    format!("{domain} {year}-{month}-{day} {hour}:{minute}:{second}")
}

/// Remove duplicate result keys, treating `www.example.com` and `example.com` as equal.
pub fn dedup_results<I: Clone>(mut results: Vec<(String, I)>) -> Vec<(String, I)> {
    let mut seen: HashSet<String> = HashSet::new();
    results.retain(|(domain, _)| {
        let key = domain.strip_prefix("www.").unwrap_or(domain).to_lowercase();
        seen.insert(key)
    });
    results
}

/// Save extracted results to a JSON file named after the source URL.
pub async fn save_results<I: Info + Clone>(
    results: &[(String, I)],
    output_path: &Path,
    source_url: &Url,
) -> Result<(), GuiError> {
    fs::create_dir_all(output_path)?;

    let items: Vec<I> = results.iter().map(|(_, info)| info.clone()).collect();

    let url_str = source_url.to_string();
    let safe_name: String = url_str
        .replace("https://", "")
        .replace("http://", "")
        .replace("://", "_")
        .replace(['/', '?', '&', '='], "_")
        .chars()
        .filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.')
        .collect::<String>()
        .trim_end_matches('_')
        .to_string();

    let json_path = output_path.join(format!("{}.json", safe_name));
    I::save_multiple(&items, &json_path)?;
    info!("💾 Saved JSON to: {}", json_path.display());

    Ok(())
}

pub const SOCIAL_MEDIA_DOMAINS: &[&str] = &[
    "facebook.com",
    "fb.com",
    "fb.me",
    "fbcdn.net",
    "twitter.com",
    "t.co",
    "twimg.com",
    "x.com",
    "linkedin.com",
    "licdn.com",
    "youtube.com",
    "youtu.be",
    "ytimg.com",
    "instagram.com",
    "instagr.am",
    "cdninstagram.com",
    "tiktok.com",
    "tiktokcdn.com",
    "snapchat.com",
    "pinterest.com",
    "pinimg.com",
    "whatsapp.com",
    "telegram.org",
    "t.me",
    "discord.com",
    "discord.gg",
    "twitch.tv",
    "vimeo.com",
];

pub const SEARCH_ENGINE_DOMAINS: &[&str] = &[
    "google.com",
    "gstatic.com",
    "googleapis.com",
    "googleusercontent.com",
    "yahoo.com",
    "yimg.com",
    "bing.com",
    "baidu.com",
    "duckduckgo.com",
];

pub const DEV_PLATFORM_DOMAINS: &[&str] = &[
    "github.com",
    "githubusercontent.com",
    "gitlab.com",
    "bitbucket.org",
];

pub const CONTENT_PLATFORM_DOMAINS: &[&str] =
    &["medium.com", "reddit.com", "redd.it", "tumblr.com"];

/// Hosted site platforms that often serve as real company websites.
///
/// Keep these separate from blocked content platforms so callers can continue
/// using the shared `is_external_url` heuristic without excluding common
/// exhibitor/SMB sites hosted on these domains.
pub const HOSTED_BUSINESS_PLATFORM_DOMAINS: &[&str] = &[
    "wordpress.com",
    "wp.com",
    "blogger.com",
    "blogspot.com",
    "wixsite.com",
    "wix.com",
    "myshopify.com",
    "shopify.com",
    "squarespace.com",
    "weebly.com",
    "webflow.io",
];

pub const REFERENCE_DOMAINS: &[&str] = &["wikipedia.org", "wikimedia.org"];

pub const MAJOR_TECH_DOMAINS: &[&str] = &[
    "maps.google.com",
    "maps.app.goo.gl",
    "goo.gl",
    "bit.ly",
    "tinyurl.com",
    "amazon.com",
    "aws.amazon.com",
    "cloudfront.net",
    "apple.com",
    "icloud.com",
    "microsoft.com",
    "live.com",
    "outlook.com",
];

/// Returns `true` if `url` is a valid http/https URL whose host is not in any
/// of the `blocked` domain groups and does not look like a CDN/asset host.
///
/// Pass any combination of the provided `*_DOMAINS` consts:
/// ```rust
/// use ras_crawl::gui::utils::{
///     is_external_url, SEARCH_ENGINE_DOMAINS, SOCIAL_MEDIA_DOMAINS,
/// };
/// use url::Url;
///
/// let url = Url::parse("https://example.com").unwrap();
/// assert!(is_external_url(
///     &url,
///     &[SOCIAL_MEDIA_DOMAINS, SEARCH_ENGINE_DOMAINS]
/// ));
/// ```
pub fn is_external_url(url: &Url, blocked: &[&[&str]]) -> bool {
    let host = match url.host_str() {
        Some(h) => h.to_lowercase(),
        None => return false,
    };

    if url.scheme() != "http" && url.scheme() != "https" {
        return false;
    }

    for group in blocked {
        for &domain in *group {
            if host == domain || host.ends_with(&format!(".{domain}")) {
                return false;
            }
        }
    }

    if host.contains("cdn.") || host.contains("static.") || host.contains("assets.") {
        return false;
    }

    host.contains('.')
}
