use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NewsSentiment {
    Bull,
    Bear,
    #[default]
    Neutral,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HighlightReason {
    BreakingKeyword,
    MacroEvent,
    StrongSentiment,
    TickerDetected,
    RecentHighValue,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct NewsArticle {
    pub id: String,
    pub source_id: String,
    pub source: String,
    pub headline: String,
    pub summary: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub canonical_url: Option<String>,
    pub published_at: String,
    pub published_ts: i64,
    pub fetched_at: String,
    pub sentiment: NewsSentiment,
    pub sentiment_score: f64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub highlight_reason: Option<HighlightReason>,
    pub tickers: Vec<String>,
    pub is_read: bool,
    pub is_saved: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct QueryNewsFeedRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ticker: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub search: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub only_highlighted: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub only_saved: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub only_unread: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub offset: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct QueryNewsFeedResponse {
    pub articles: Vec<NewsArticle>,
    pub total: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_synced_at: Option<String>,
    pub sources: Vec<NewsSourceStatus>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
pub struct RefreshNewsFeedRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub force: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RefreshNewsFeedResult {
    pub feeds_checked: usize,
    pub feeds_succeeded: usize,
    pub feeds_failed: usize,
    pub new_articles: usize,
    pub updated_articles: usize,
    pub unchanged_articles: usize,
    pub finished_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SaveNewsFeedConfigRequest {
    pub feeds: Vec<NewsSourceConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UpdateNewsArticleStateRequest {
    pub article_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_read: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_saved: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NewsSourceConfig {
    pub id: String,
    pub name: String,
    pub url: String,
    pub refresh_minutes: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NewsSourceConfigFile {
    pub feeds: Vec<NewsSourceConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NewsSourceStatus {
    pub id: String,
    pub name: String,
    pub url: String,
    pub refresh_minutes: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_checked_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_success_at: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_error: Option<String>,
    pub failure_count: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FeedSourceRecord {
    pub id: String,
    pub name: String,
    pub url: String,
    pub refresh_minutes: u32,
    pub etag: Option<String>,
    pub last_modified: Option<String>,
    pub last_checked_at: Option<String>,
    pub last_success_at: Option<String>,
    pub last_error: Option<String>,
    pub failure_count: u32,
}

impl FeedSourceRecord {
    pub(crate) fn is_due(&self, now: DateTime<Utc>) -> bool {
        let Some(last_checked_at) = self.last_checked_at.as_deref() else {
            return true;
        };

        DateTime::parse_from_rfc3339(last_checked_at)
            .map(|value| value.with_timezone(&Utc))
            .map(|value| now >= value + chrono::Duration::minutes(i64::from(self.refresh_minutes)))
            .unwrap_or(true)
    }
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ParsedFeed {
    pub articles: Vec<ParsedNewsArticle>,
    pub malformed_entries: usize,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ParsedNewsArticle {
    pub source_id: String,
    pub source: String,
    pub headline: String,
    pub summary: String,
    pub url: Option<String>,
    pub canonical_url: Option<String>,
    pub published_at: String,
    pub published_ts: i64,
    pub fetched_at: String,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ClassifiedNewsArticle {
    pub fingerprint: String,
    pub source_id: String,
    pub source: String,
    pub headline: String,
    pub summary: String,
    pub url: Option<String>,
    pub canonical_url: Option<String>,
    pub published_at: String,
    pub published_ts: i64,
    pub fetched_at: String,
    pub sentiment: NewsSentiment,
    pub sentiment_score: f64,
    pub highlight_reason: Option<HighlightReason>,
    pub tickers: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FetchResultKind {
    Updated,
    NotModified,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FetchedFeed {
    pub kind: FetchResultKind,
    pub body: Option<String>,
    pub etag: Option<String>,
    pub last_modified: Option<String>,
    pub checked_at: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct ArticleUpsertSummary {
    pub new_articles: usize,
    pub updated_articles: usize,
    pub unchanged_articles: usize,
}
