use std::path::PathBuf;
use std::sync::Arc;

use chrono::Utc;
use futures::stream::{self, StreamExt};

use crate::news::classifier::classify_article;
use crate::news::config::{load_or_bootstrap_config, reset_config, save_config};
use crate::news::fetcher::FeedFetcher;
use crate::news::parser::parse_feed;
use crate::news::repository::NewsRepository;
use crate::news::types::{
    ArticleUpsertSummary, FeedSourceRecord, FetchResultKind, NewsSourceConfig,
    QueryNewsFeedRequest, QueryNewsFeedResponse, RefreshNewsFeedRequest, RefreshNewsFeedResult,
    SaveNewsFeedConfigRequest, UpdateNewsArticleStateRequest,
};
use crate::news::{NewsError, Result};

#[derive(Clone)]
pub struct NewsService {
    repository: Arc<NewsRepository>,
    fetcher: FeedFetcher,
    config_path: PathBuf,
    default_config_bytes: Arc<Vec<u8>>,
}

impl NewsService {
    pub fn new(
        db_path: PathBuf,
        config_path: PathBuf,
        default_config_bytes: &[u8],
    ) -> Result<Self> {
        Self::with_fetcher(
            db_path,
            config_path,
            default_config_bytes,
            FeedFetcher::new()?,
        )
    }

    pub(crate) fn with_fetcher(
        db_path: PathBuf,
        config_path: PathBuf,
        default_config_bytes: &[u8],
        fetcher: FeedFetcher,
    ) -> Result<Self> {
        let feeds = load_or_bootstrap_config(&config_path, default_config_bytes)?;
        let repository = Arc::new(NewsRepository::new(db_path)?);
        repository.sync_sources_blocking(feeds)?;

        Ok(Self {
            repository,
            fetcher,
            config_path,
            default_config_bytes: Arc::new(default_config_bytes.to_vec()),
        })
    }

    pub async fn query_feed(&self, request: QueryNewsFeedRequest) -> Result<QueryNewsFeedResponse> {
        self.repository.query_articles(request).await
    }

    pub async fn refresh_feed(
        &self,
        request: RefreshNewsFeedRequest,
    ) -> Result<RefreshNewsFeedResult> {
        let force = request.force.unwrap_or(false);
        let feeds = load_or_bootstrap_config(&self.config_path, &self.default_config_bytes)?;
        self.repository.sync_sources(feeds).await?;

        let now = Utc::now();
        let sources = self.repository.list_sources().await?;
        let due_sources = sources
            .into_iter()
            .filter(|source| force || source.is_due(now))
            .collect::<Vec<_>>();

        if due_sources.is_empty() {
            return Ok(RefreshNewsFeedResult {
                feeds_checked: 0,
                feeds_succeeded: 0,
                feeds_failed: 0,
                new_articles: 0,
                updated_articles: 0,
                unchanged_articles: 0,
                finished_at: now.to_rfc3339(),
            });
        }

        let outcomes = stream::iter(due_sources)
            .map(|source| {
                let service = self.clone();
                async move { service.refresh_source(source, force).await }
            })
            .buffer_unordered(4)
            .collect::<Vec<_>>()
            .await;

        let mut result = RefreshNewsFeedResult {
            feeds_checked: 0,
            feeds_succeeded: 0,
            feeds_failed: 0,
            new_articles: 0,
            updated_articles: 0,
            unchanged_articles: 0,
            finished_at: Utc::now().to_rfc3339(),
        };

        for outcome in outcomes {
            result.feeds_checked += 1;
            if outcome.succeeded {
                result.feeds_succeeded += 1;
            } else {
                result.feeds_failed += 1;
            }
            result.new_articles += outcome.upsert_summary.new_articles;
            result.updated_articles += outcome.upsert_summary.updated_articles;
            result.unchanged_articles += outcome.upsert_summary.unchanged_articles;
        }

        Ok(result)
    }

    pub async fn update_article_state(&self, request: UpdateNewsArticleStateRequest) -> Result<()> {
        self.repository.update_article_state(request).await
    }

    pub async fn get_feed_config(&self) -> Result<Vec<NewsSourceConfig>> {
        let config_path = self.config_path.clone();
        let default_config_bytes = self.default_config_bytes.clone();
        self.run_config_io(move || load_or_bootstrap_config(&config_path, &default_config_bytes))
            .await
    }

    pub async fn save_feed_config(
        &self,
        request: SaveNewsFeedConfigRequest,
    ) -> Result<Vec<NewsSourceConfig>> {
        let config_path = self.config_path.clone();
        let feeds = self
            .run_config_io(move || save_config(&config_path, &request.feeds))
            .await?;
        self.repository.sync_sources(feeds.clone()).await?;
        Ok(feeds)
    }

    pub async fn reset_feed_config(&self) -> Result<Vec<NewsSourceConfig>> {
        let config_path = self.config_path.clone();
        let default_config_bytes = self.default_config_bytes.clone();
        let feeds = self
            .run_config_io(move || reset_config(&config_path, &default_config_bytes))
            .await?;
        self.repository.sync_sources(feeds.clone()).await?;
        Ok(feeds)
    }

    async fn refresh_source(&self, source: FeedSourceRecord, force: bool) -> RefreshOutcome {
        let fetched = match self.fetcher.fetch(&source, force).await {
            Ok(value) => value,
            Err(error) => {
                let checked_at = Utc::now().to_rfc3339();
                let _ = self
                    .repository
                    .record_fetch_failure(source.id, checked_at, error.to_string())
                    .await;
                return RefreshOutcome::failed();
            }
        };

        if fetched.kind == FetchResultKind::NotModified {
            let _ = self
                .repository
                .record_fetch_success(
                    source.id,
                    fetched.checked_at,
                    fetched.etag,
                    fetched.last_modified,
                )
                .await;
            return RefreshOutcome::succeeded(ArticleUpsertSummary::default());
        }

        let Some(body) = fetched.body.as_deref() else {
            let _ = self
                .repository
                .record_fetch_failure(
                    source.id,
                    fetched.checked_at,
                    "feed body missing after successful fetch".to_string(),
                )
                .await;
            return RefreshOutcome::failed();
        };

        let parsed = match parse_feed(&source, body, &fetched.checked_at) {
            Ok(value) => value,
            Err(error) => {
                let _ = self
                    .repository
                    .record_fetch_failure(source.id, fetched.checked_at, error.to_string())
                    .await;
                return RefreshOutcome::failed();
            }
        };

        let articles = parsed
            .articles
            .into_iter()
            .map(classify_article)
            .collect::<Vec<_>>();
        let upsert_summary = match self.repository.upsert_articles(articles).await {
            Ok(value) => value,
            Err(error) => {
                let _ = self
                    .repository
                    .record_fetch_failure(source.id, fetched.checked_at, error.to_string())
                    .await;
                return RefreshOutcome::failed();
            }
        };

        let _ = self
            .repository
            .record_fetch_success(
                source.id,
                fetched.checked_at,
                fetched.etag,
                fetched.last_modified,
            )
            .await;
        RefreshOutcome::succeeded(upsert_summary)
    }

    async fn run_config_io<F, T>(&self, task: F) -> Result<T>
    where
        F: FnOnce() -> Result<T> + Send + 'static,
        T: Send + 'static,
    {
        tokio::task::spawn_blocking(task)
            .await
            .map_err(|error| NewsError::Join(error.to_string()))?
    }
}

struct RefreshOutcome {
    succeeded: bool,
    upsert_summary: ArticleUpsertSummary,
}

impl RefreshOutcome {
    fn succeeded(upsert_summary: ArticleUpsertSummary) -> Self {
        Self {
            succeeded: true,
            upsert_summary,
        }
    }

    fn failed() -> Self {
        Self {
            succeeded: false,
            upsert_summary: ArticleUpsertSummary::default(),
        }
    }
}

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

    use tempfile::tempdir;
    use wiremock::matchers::{header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    use super::NewsService;
    use crate::news::fetcher::FeedFetcher;
    use crate::news::types::{
        NewsSourceConfig, QueryNewsFeedRequest, RefreshNewsFeedRequest, SaveNewsFeedConfigRequest,
    };

    #[tokio::test]
    async fn refresh_feed_should_continue_when_one_feed_times_out() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/ok.xml"))
            .respond_with(
                ResponseTemplate::new(200)
                    .insert_header("content-type", "application/rss+xml")
                    .set_body_string(include_str!("../../tests/fixtures/news/sample.rss")),
            )
            .mount(&server)
            .await;
        Mock::given(method("GET"))
            .and(path("/slow.xml"))
            .respond_with(
                ResponseTemplate::new(200)
                    .set_delay(Duration::from_millis(150))
                    .set_body_string(include_str!("../../tests/fixtures/news/sample.rss")),
            )
            .mount(&server)
            .await;

        let temp_dir = tempdir().unwrap();
        let config = format!(
            r#"{{
                "feeds": [
                    {{"id":"ok","name":"OK Feed","url":"{}/ok.xml","refreshMinutes":15}},
                    {{"id":"slow","name":"Slow Feed","url":"{}/slow.xml","refreshMinutes":15}}
                ]
            }}"#,
            server.uri(),
            server.uri(),
        );
        let service = NewsService::with_fetcher(
            temp_dir.path().join("news.sqlite"),
            temp_dir.path().join("news-feeds.json"),
            config.as_bytes(),
            FeedFetcher::with_timeout(Duration::from_millis(50)).unwrap(),
        )
        .unwrap();

        let result = service
            .refresh_feed(RefreshNewsFeedRequest { force: Some(true) })
            .await
            .unwrap();
        let response = service
            .query_feed(QueryNewsFeedRequest {
                ticker: None,
                search: None,
                only_highlighted: None,
                only_saved: None,
                only_unread: None,
                limit: Some(10),
                offset: Some(0),
            })
            .await
            .unwrap();

        assert_eq!(result.feeds_checked, 2);
        assert_eq!(result.feeds_succeeded, 1);
        assert_eq!(result.feeds_failed, 1);
        assert_eq!(response.total, 2);
    }

    #[tokio::test]
    async fn save_feed_config_should_replace_runtime_sources() {
        let temp_dir = tempdir().unwrap();
        let db_path = temp_dir.path().join("news.sqlite");
        let config_path = temp_dir.path().join("news-feeds.json");
        let service = NewsService::with_fetcher(
            db_path,
            config_path,
            br#"{"feeds":[
                {"id":"fed","name":"Fed","url":"https://example.com/fed.xml","refreshMinutes":15},
                {"id":"sec","name":"SEC","url":"https://example.com/sec.xml","refreshMinutes":15}
            ]}"#,
            FeedFetcher::new().unwrap(),
        )
        .unwrap();

        let feeds = service
            .save_feed_config(SaveNewsFeedConfigRequest {
                feeds: vec![NewsSourceConfig {
                    id: "custom".to_string(),
                    name: "Custom".to_string(),
                    url: "https://example.com/custom.xml".to_string(),
                    refresh_minutes: 45,
                }],
            })
            .await
            .unwrap();

        let sources = service
            .query_feed(QueryNewsFeedRequest {
                ticker: None,
                search: None,
                only_highlighted: None,
                only_saved: None,
                only_unread: None,
                limit: Some(10),
                offset: Some(0),
            })
            .await
            .unwrap()
            .sources;
        assert_eq!(feeds.len(), 1);
        assert_eq!(sources.len(), 1);
        assert_eq!(sources[0].id, "custom");
    }

    #[tokio::test]
    async fn refresh_source_should_use_conditional_get_headers_after_initial_sync() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/etag.xml"))
            .respond_with(
                ResponseTemplate::new(200)
                    .insert_header("etag", "v1")
                    .set_body_string(include_str!("../../tests/fixtures/news/sample.rss")),
            )
            .mount(&server)
            .await;

        let temp_dir = tempdir().unwrap();
        let config = format!(
            r#"{{"feeds":[{{"id":"etag","name":"ETag Feed","url":"{}/etag.xml","refreshMinutes":15}}]}}"#,
            server.uri(),
        );
        let service = NewsService::with_fetcher(
            temp_dir.path().join("news.sqlite"),
            temp_dir.path().join("news-feeds.json"),
            config.as_bytes(),
            FeedFetcher::with_timeout(Duration::from_millis(100)).unwrap(),
        )
        .unwrap();

        service
            .refresh_feed(RefreshNewsFeedRequest { force: Some(true) })
            .await
            .unwrap();

        server.reset().await;
        Mock::given(method("GET"))
            .and(path("/etag.xml"))
            .and(header("if-none-match", "v1"))
            .respond_with(ResponseTemplate::new(304))
            .mount(&server)
            .await;

        let source = service
            .repository
            .list_sources()
            .await
            .unwrap()
            .into_iter()
            .next()
            .unwrap();
        let outcome = service.refresh_source(source, false).await;

        assert!(outcome.succeeded);
        assert_eq!(outcome.upsert_summary.new_articles, 0);
    }

    #[tokio::test]
    async fn startup_should_create_database_schema_on_empty_path() {
        let temp_dir = tempdir().unwrap();
        let db_path = temp_dir.path().join("news.sqlite");
        let config_path = temp_dir.path().join("news-feeds.json");

        let service = NewsService::new(
            db_path.clone(),
            config_path,
            br#"{"feeds":[{"id":"sample","name":"Sample Feed","url":"https://example.com/feed.xml","refreshMinutes":15}]}"#,
        )
        .unwrap();

        let metadata = fs::metadata(db_path).unwrap();
        let response = service
            .query_feed(QueryNewsFeedRequest {
                ticker: None,
                search: None,
                only_highlighted: None,
                only_saved: None,
                only_unread: None,
                limit: Some(10),
                offset: Some(0),
            })
            .await
            .unwrap();

        assert!(metadata.is_file());
        assert_eq!(response.total, 0);
    }
}
