use std::time::Duration;

use chrono::Utc;
use reqwest::header::{ETAG, IF_MODIFIED_SINCE, IF_NONE_MATCH, LAST_MODIFIED, USER_AGENT};
use reqwest::{Client, StatusCode};

use crate::news::types::{FeedSourceRecord, FetchResultKind, FetchedFeed};
use crate::news::{NewsError, Result};

const DEFAULT_USER_AGENT: &str = "MosaicIQ/0.1 (local-first-news)";

#[derive(Clone)]
pub struct FeedFetcher {
    client: Client,
}

impl FeedFetcher {
    pub fn new() -> Result<Self> {
        Self::with_timeout(Duration::from_secs(8))
    }

    pub fn with_timeout(timeout: Duration) -> Result<Self> {
        let client = Client::builder().timeout(timeout).build()?;

        Ok(Self { client })
    }

    pub async fn fetch(&self, source: &FeedSourceRecord, force: bool) -> Result<FetchedFeed> {
        let checked_at = Utc::now().to_rfc3339();
        let mut request = self
            .client
            .get(&source.url)
            .header(USER_AGENT, DEFAULT_USER_AGENT);

        if !force {
            if let Some(etag) = source.etag.as_deref() {
                request = request.header(IF_NONE_MATCH, etag);
            }
            if let Some(last_modified) = source.last_modified.as_deref() {
                request = request.header(IF_MODIFIED_SINCE, last_modified);
            }
        }

        let response = request.send().await?;
        let etag = response
            .headers()
            .get(ETAG)
            .and_then(|value| value.to_str().ok())
            .map(ToString::to_string);
        let last_modified = response
            .headers()
            .get(LAST_MODIFIED)
            .and_then(|value| value.to_str().ok())
            .map(ToString::to_string);

        if response.status() == StatusCode::NOT_MODIFIED {
            return Ok(FetchedFeed {
                kind: FetchResultKind::NotModified,
                body: None,
                etag,
                last_modified,
                checked_at,
            });
        }

        if !response.status().is_success() {
            let status = response.status();
            let detail = response.text().await.unwrap_or_default();
            return Err(NewsError::Parse(format!(
                "feed {} returned {} {}",
                source.id, status, detail
            )));
        }

        Ok(FetchedFeed {
            kind: FetchResultKind::Updated,
            body: Some(response.text().await?),
            etag,
            last_modified,
            checked_at,
        })
    }
}
