use std::collections::HashSet;
use std::fs;
use std::path::Path;

use crate::news::types::{NewsSourceConfig, NewsSourceConfigFile};
use crate::news::{NewsError, Result};

pub fn load_or_bootstrap_config(
    config_path: &Path,
    default_config_bytes: &[u8],
) -> Result<Vec<NewsSourceConfig>> {
    if !config_path.exists() {
        bootstrap_config(config_path, default_config_bytes)?;
    }

    load_config(config_path)
}

pub fn load_config(config_path: &Path) -> Result<Vec<NewsSourceConfig>> {
    let config = serde_json::from_slice::<NewsSourceConfigFile>(&fs::read(config_path)?)?;
    validate_config(&config.feeds)?;
    Ok(config.feeds)
}

pub fn save_config(
    config_path: &Path,
    feeds: &[NewsSourceConfig],
) -> Result<Vec<NewsSourceConfig>> {
    validate_config(feeds)?;
    let Some(parent) = config_path.parent() else {
        return Err(NewsError::Config(format!(
            "config path has no parent: {}",
            config_path.display()
        )));
    };
    fs::create_dir_all(parent)?;
    let file = NewsSourceConfigFile {
        feeds: feeds.to_vec(),
    };
    fs::write(config_path, serde_json::to_vec_pretty(&file)?)?;
    Ok(file.feeds)
}

pub fn reset_config(
    config_path: &Path,
    default_config_bytes: &[u8],
) -> Result<Vec<NewsSourceConfig>> {
    bootstrap_config(config_path, default_config_bytes)?;
    load_config(config_path)
}

fn bootstrap_config(config_path: &Path, default_config_bytes: &[u8]) -> Result<()> {
    let Some(parent) = config_path.parent() else {
        return Err(NewsError::Config(format!(
            "config path has no parent: {}",
            config_path.display()
        )));
    };
    fs::create_dir_all(parent)?;
    fs::write(config_path, default_config_bytes)?;
    Ok(())
}

fn validate_config(feeds: &[NewsSourceConfig]) -> Result<()> {
    if feeds.is_empty() {
        return Err(NewsError::Config(
            "feed configuration must contain at least one feed".to_string(),
        ));
    }

    let mut seen_ids = HashSet::new();
    for feed in feeds {
        if feed.id.trim().is_empty() {
            return Err(NewsError::Config("feed id cannot be empty".to_string()));
        }
        if feed.name.trim().is_empty() {
            return Err(NewsError::Config(format!(
                "feed {} has an empty name",
                feed.id
            )));
        }
        if !(feed.url.starts_with("http://") || feed.url.starts_with("https://")) {
            return Err(NewsError::Config(format!(
                "feed {} must use http or https",
                feed.id
            )));
        }
        if feed.refresh_minutes == 0 {
            return Err(NewsError::Config(format!(
                "feed {} must use a positive refreshMinutes",
                feed.id
            )));
        }
        if !seen_ids.insert(feed.id.clone()) {
            return Err(NewsError::Config(format!(
                "duplicate feed id in config: {}",
                feed.id
            )));
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::{load_or_bootstrap_config, reset_config, save_config};
    use crate::news::types::NewsSourceConfig;

    #[test]
    fn load_or_bootstrap_config_should_copy_default_file_when_missing() {
        let temp_dir = tempdir().unwrap();
        let config_path = temp_dir.path().join("news-feeds.json");

        let feeds = load_or_bootstrap_config(
            &config_path,
            br#"{"feeds":[{"id":"fed","name":"Fed","url":"https://example.com","refreshMinutes":15}]}"#,
        )
        .unwrap();

        assert_eq!(feeds.len(), 1);
        assert!(fs::metadata(config_path).is_ok());
    }

    #[test]
    fn save_config_should_reject_duplicate_ids() {
        let temp_dir = tempdir().unwrap();
        let config_path = temp_dir.path().join("news-feeds.json");

        let error = save_config(
            &config_path,
            &[
                NewsSourceConfig {
                    id: "dup".to_string(),
                    name: "Feed A".to_string(),
                    url: "https://example.com/a.xml".to_string(),
                    refresh_minutes: 15,
                },
                NewsSourceConfig {
                    id: "dup".to_string(),
                    name: "Feed B".to_string(),
                    url: "https://example.com/b.xml".to_string(),
                    refresh_minutes: 30,
                },
            ],
        )
        .unwrap_err();

        assert!(error.to_string().contains("duplicate feed id"));
    }

    #[test]
    fn reset_config_should_restore_default_file_contents() {
        let temp_dir = tempdir().unwrap();
        let config_path = temp_dir.path().join("news-feeds.json");
        fs::write(
            &config_path,
            br#"{"feeds":[{"id":"custom","name":"Custom","url":"https://example.com/custom.xml","refreshMinutes":30}]}"#,
        )
        .unwrap();

        let feeds = reset_config(
            &config_path,
            br#"{"feeds":[{"id":"fed","name":"Fed","url":"https://example.com/fed.xml","refreshMinutes":15}]}"#,
        )
        .unwrap();

        assert_eq!(feeds[0].id, "fed");
    }
}
