use chrono::{Duration, Utc};
use loco_rs::prelude::*;
use reqwest::Client;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, Order, QueryFilter, QueryOrder,
    QuerySelect, Set,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;

use crate::{
    mailers::subscription::SubscriptionMailer,
    models::_entities::{notification_deliveries, posts, subscriptions},
    services::{content, web_push as web_push_service, worker_jobs},
};

pub const CHANNEL_EMAIL: &str = "email";
pub const CHANNEL_WEBHOOK: &str = "webhook";
pub const CHANNEL_DISCORD: &str = "discord";
pub const CHANNEL_TELEGRAM: &str = "telegram";
pub const CHANNEL_NTFY: &str = "ntfy";
pub const CHANNEL_WEB_PUSH: &str = "web_push";

pub const STATUS_PENDING: &str = "pending";
pub const STATUS_ACTIVE: &str = "active";
pub const STATUS_PAUSED: &str = "paused";
pub const STATUS_UNSUBSCRIBED: &str = "unsubscribed";

pub const EVENT_POST_PUBLISHED: &str = "post.published";
pub const EVENT_DIGEST_WEEKLY: &str = "digest.weekly";
pub const EVENT_DIGEST_MONTHLY: &str = "digest.monthly";
pub const EVENT_SUBSCRIPTION_TEST: &str = "subscription.test";
pub const EVENT_COMMENT_CREATED: &str = "comment.created";
pub const EVENT_FRIEND_LINK_CREATED: &str = "friend_link.created";

pub const DELIVERY_STATUS_QUEUED: &str = "queued";
pub const DELIVERY_STATUS_SENT: &str = "sent";
pub const DELIVERY_STATUS_RETRY_PENDING: &str = "retry_pending";
pub const DELIVERY_STATUS_EXHAUSTED: &str = "exhausted";
pub const DELIVERY_STATUS_SKIPPED: &str = "skipped";

const WEB_PUSH_TITLE_MAX_CHARS: usize = 72;
const WEB_PUSH_BODY_MAX_CHARS: usize = 160;
const WEB_PUSH_MAX_PAYLOAD_BYTES: usize = 2800;
const WEB_PUSH_AUTO_PAUSE_FAILURE_THRESHOLD: i32 = 2;
const WEB_PUSH_AUTO_PAUSE_NOTE: &str =
    "浏览器推送订阅连续投递失败，系统已自动暂停。请在浏览器里重新开启提醒。";

#[derive(Clone, Debug, Serialize)]
pub struct DigestDispatchSummary {
    pub period: String,
    pub post_count: usize,
    pub queued: usize,
    pub skipped: usize,
}

#[derive(Clone, Debug, Serialize)]
pub struct QueueDispatchSummary {
    pub queued: usize,
    pub skipped: usize,
}

#[derive(Clone, Debug, Serialize)]
pub struct PublicSubscriptionView {
    pub created_at: chrono::DateTime<chrono::FixedOffset>,
    pub updated_at: chrono::DateTime<chrono::FixedOffset>,
    pub id: i32,
    pub channel_type: String,
    pub target: String,
    pub display_name: Option<String>,
    pub status: String,
    pub filters: Option<Value>,
    pub metadata: Option<Value>,
    pub verified_at: Option<String>,
    pub last_notified_at: Option<String>,
    pub last_delivery_status: Option<String>,
    pub manage_token: Option<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct PublicSubscriptionResult {
    pub subscription: PublicSubscriptionView,
    pub requires_confirmation: bool,
    pub message: String,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
struct DeliveryEnvelope {
    event: String,
    site_name: Option<String>,
    site_url: Option<String>,
    timestamp: String,
    subject: String,
    text: String,
    data: Value,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
struct QueuedDeliveryPayload {
    subject: String,
    headline: String,
    text: String,
    site_name: Option<String>,
    site_url: Option<String>,
    payload: Value,
    manage_url: Option<String>,
    unsubscribe_url: Option<String>,
}

#[derive(Clone, Debug)]
struct SiteContext {
    site_name: Option<String>,
    site_url: Option<String>,
}

fn trim_to_option(value: Option<String>) -> Option<String> {
    value.and_then(|item| {
        let trimmed = item.trim().to_string();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed)
        }
    })
}

fn normalize_string(value: &str) -> String {
    value.trim().to_ascii_lowercase()
}

pub fn generate_subscription_token() -> String {
    Uuid::new_v4().simple().to_string()
}

pub fn normalize_channel_type(value: &str) -> String {
    match value.trim().to_ascii_lowercase().as_str() {
        CHANNEL_WEBHOOK => CHANNEL_WEBHOOK.to_string(),
        CHANNEL_DISCORD => CHANNEL_DISCORD.to_string(),
        CHANNEL_TELEGRAM => CHANNEL_TELEGRAM.to_string(),
        CHANNEL_NTFY => CHANNEL_NTFY.to_string(),
        CHANNEL_WEB_PUSH | "browser_push" | "browser-push" | "webpush" => {
            CHANNEL_WEB_PUSH.to_string()
        }
        _ => CHANNEL_EMAIL.to_string(),
    }
}

pub fn normalize_status(value: &str) -> String {
    match value.trim().to_ascii_lowercase().as_str() {
        STATUS_PENDING => STATUS_PENDING.to_string(),
        STATUS_PAUSED => STATUS_PAUSED.to_string(),
        STATUS_UNSUBSCRIBED => STATUS_UNSUBSCRIBED.to_string(),
        _ => STATUS_ACTIVE.to_string(),
    }
}

fn default_public_filters() -> Value {
    serde_json::json!({
        "event_types": [
            EVENT_POST_PUBLISHED,
            EVENT_DIGEST_WEEKLY,
            EVENT_DIGEST_MONTHLY,
        ]
    })
}

fn normalize_string_list(items: &[Value]) -> Vec<Value> {
    let mut seen = std::collections::HashSet::new();
    items
        .iter()
        .filter_map(Value::as_str)
        .map(normalize_string)
        .filter(|item| !item.is_empty())
        .filter(|item| seen.insert(item.clone()))
        .map(Value::String)
        .collect()
}

pub fn normalize_filters(value: Option<Value>) -> Option<Value> {
    let normalized = match value {
        None | Some(Value::Null) => None,
        Some(Value::Array(items)) => Some(serde_json::json!({
            "event_types": normalize_string_list(&items)
        })),
        Some(Value::Object(mut object)) => {
            if let Some(items) = object.get("event_types").and_then(Value::as_array) {
                object.insert(
                    "event_types".to_string(),
                    Value::Array(normalize_string_list(items)),
                );
            }
            if let Some(items) = object.get("categories").and_then(Value::as_array) {
                object.insert(
                    "categories".to_string(),
                    Value::Array(normalize_string_list(items)),
                );
            }
            if let Some(items) = object.get("tags").and_then(Value::as_array) {
                object.insert(
                    "tags".to_string(),
                    Value::Array(normalize_string_list(items)),
                );
            }
            Some(Value::Object(object))
        }
        Some(other) => Some(other),
    };

    normalized.and_then(|value| match value {
        Value::Object(object) if object.is_empty() => None,
        other => Some(other),
    })
}

fn merge_metadata(existing: Option<&Value>, incoming: Option<Value>) -> Option<Value> {
    match (existing, incoming) {
        (Some(Value::Object(left)), Some(Value::Object(right))) => {
            let mut merged = left.clone();
            for (key, value) in right {
                merged.insert(key, value);
            }
            Some(Value::Object(merged))
        }
        (_, Some(value)) => Some(value),
        (Some(value), None) => Some(value.clone()),
        (None, None) => None,
    }
}

fn normalize_browser_push_subscription(raw: Value) -> Result<Value> {
    let mut subscription = serde_json::from_value::<web_push::SubscriptionInfo>(raw)
        .map_err(|_| Error::BadRequest("browser push subscription 非法".to_string()))?;

    subscription.endpoint = subscription.endpoint.trim().to_string();
    subscription.keys.p256dh = subscription.keys.p256dh.trim().to_string();
    subscription.keys.auth = subscription.keys.auth.trim().to_string();

    if subscription.endpoint.is_empty()
        || subscription.keys.p256dh.is_empty()
        || subscription.keys.auth.is_empty()
    {
        return Err(Error::BadRequest(
            "browser push subscription 缺少 endpoint / keys".to_string(),
        ));
    }

    serde_json::to_value(subscription).map_err(Into::into)
}

fn merge_browser_push_metadata(
    existing: Option<&Value>,
    incoming: Option<Value>,
    subscription: Value,
) -> Value {
    let mut object = merge_metadata(existing, incoming)
        .and_then(|value| value.as_object().cloned())
        .unwrap_or_default();
    object.insert(
        "kind".to_string(),
        Value::String("browser-push".to_string()),
    );
    object.insert("subscription".to_string(), subscription);
    Value::Object(object)
}

fn merge_subscription_note(existing: Option<&str>, note: &str) -> Option<String> {
    let note = note.trim();
    let mut lines = existing
        .unwrap_or_default()
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(ToString::to_string)
        .collect::<Vec<_>>();

    if !note.is_empty() && !lines.iter().any(|line| line == note) {
        lines.push(note.to_string());
    }

    if lines.is_empty() {
        None
    } else {
        Some(lines.join("\n"))
    }
}

fn remove_subscription_note(existing: Option<&str>, note: &str) -> Option<String> {
    let note = note.trim();
    let lines = existing
        .unwrap_or_default()
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty() && *line != note)
        .map(ToString::to_string)
        .collect::<Vec<_>>();

    if lines.is_empty() {
        None
    } else {
        Some(lines.join("\n"))
    }
}

fn web_push_error_looks_terminal(error_text: &str) -> bool {
    let normalized = error_text.trim().to_ascii_lowercase();

    normalized.contains("endpoint_host=fcm.googleapis.com")
        && normalized.contains("unspecified error")
        || normalized.contains("410")
        || normalized.contains("404")
        || normalized.contains("gone")
        || normalized.contains("not found")
        || normalized.contains("expired")
        || normalized.contains("unsubscribed")
        || normalized.contains("invalid subscription")
        || normalized.contains("push subscription")
}

fn should_auto_pause_failed_web_push_subscription(
    failure_count_after_error: i32,
    error_text: &str,
) -> bool {
    failure_count_after_error >= WEB_PUSH_AUTO_PAUSE_FAILURE_THRESHOLD
        || web_push_error_looks_terminal(error_text)
}

async fn maybe_pause_failed_web_push_subscription(
    ctx: &AppContext,
    subscription: Option<&subscriptions::Model>,
    error_text: &str,
) -> Result<()> {
    let Some(subscription) = subscription else {
        return Ok(());
    };

    if subscription.channel_type != CHANNEL_WEB_PUSH
        || normalize_status(&subscription.status) != STATUS_ACTIVE
    {
        return Ok(());
    }

    let failure_count_after_error = subscription.failure_count.unwrap_or(0) + 1;
    if !should_auto_pause_failed_web_push_subscription(failure_count_after_error, error_text) {
        return Ok(());
    }

    let mut active = subscription.clone().into_active_model();
    active.status = Set(STATUS_PAUSED.to_string());
    active.notes = Set(merge_subscription_note(
        subscription.notes.as_deref(),
        WEB_PUSH_AUTO_PAUSE_NOTE,
    ));
    let _ = active.update(&ctx.db).await?;
    Ok(())
}

fn json_string_list(value: Option<&Value>, key: &str) -> Vec<String> {
    value
        .and_then(Value::as_object)
        .and_then(|object| object.get(key))
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(Value::as_str)
                .map(normalize_string)
                .filter(|item| !item.is_empty())
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn payload_match_strings(payload: &Value, key: &str) -> Vec<String> {
    let mut values = Vec::new();

    if let Some(item) = payload.get(key).and_then(Value::as_str) {
        let normalized = normalize_string(item);
        if !normalized.is_empty() {
            values.push(normalized);
        }
    }

    if let Some(items) = payload.get(key).and_then(Value::as_array) {
        values.extend(
            items
                .iter()
                .filter_map(Value::as_str)
                .map(normalize_string)
                .filter(|item| !item.is_empty()),
        );
    }

    if let Some(posts) = payload.get("posts").and_then(Value::as_array) {
        for post in posts {
            if let Some(item) = post.get(key).and_then(Value::as_str) {
                let normalized = normalize_string(item);
                if !normalized.is_empty() {
                    values.push(normalized);
                }
            }

            if let Some(items) = post.get(key).and_then(Value::as_array) {
                values.extend(
                    items
                        .iter()
                        .filter_map(Value::as_str)
                        .map(normalize_string)
                        .filter(|item| !item.is_empty()),
                );
            }
        }
    }

    values.sort();
    values.dedup();
    values
}

fn effective_period(period: &str) -> (&'static str, i64, &'static str) {
    match period.trim().to_ascii_lowercase().as_str() {
        "monthly" | "month" | "30d" => ("monthly", 30, EVENT_DIGEST_MONTHLY),
        _ => ("weekly", 7, EVENT_DIGEST_WEEKLY),
    }
}

fn trim_site_url(value: Option<String>) -> Option<String> {
    trim_to_option(value).map(|item| item.trim_end_matches('/').to_string())
}

fn build_token_link(site_url: Option<&str>, path: &str, token: &str) -> Option<String> {
    let base = site_url?.trim().trim_end_matches('/');
    if base.is_empty() {
        return None;
    }

    Some(format!("{base}{path}?token={token}"))
}

fn post_public_url(site_url: Option<&str>, slug: &str) -> Option<String> {
    let site_url = site_url?.trim().trim_end_matches('/');
    if site_url.is_empty() {
        return None;
    }

    Some(format!("{site_url}/articles/{slug}"))
}

async fn load_site_context(ctx: &AppContext) -> SiteContext {
    match crate::controllers::site_settings::load_current(ctx).await {
        Ok(settings) => SiteContext {
            site_name: trim_to_option(settings.site_name),
            site_url: trim_site_url(settings.site_url),
        },
        Err(error) => {
            tracing::warn!("failed to load site settings for subscriptions: {error}");
            SiteContext {
                site_name: Some("Termi".to_string()),
                site_url: Some(ctx.config.server.full_url()),
            }
        }
    }
}

pub async fn ensure_subscription_tokens(
    ctx: &AppContext,
    item: &subscriptions::Model,
) -> Result<subscriptions::Model> {
    let mut active = item.clone().into_active_model();
    let mut changed = false;

    if item
        .manage_token
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .is_none()
    {
        active.manage_token = Set(Some(generate_subscription_token()));
        changed = true;
    }

    if changed {
        active.update(&ctx.db).await.map_err(Into::into)
    } else {
        Ok(item.clone())
    }
}

pub fn to_public_subscription_view(item: &subscriptions::Model) -> PublicSubscriptionView {
    PublicSubscriptionView {
        created_at: item.created_at.clone(),
        updated_at: item.updated_at.clone(),
        id: item.id,
        channel_type: item.channel_type.clone(),
        target: item.target.clone(),
        display_name: item.display_name.clone(),
        status: item.status.clone(),
        filters: item.filters.clone(),
        metadata: item.metadata.clone(),
        verified_at: item.verified_at.clone(),
        last_notified_at: item.last_notified_at.clone(),
        last_delivery_status: item.last_delivery_status.clone(),
        manage_token: item.manage_token.clone(),
    }
}

fn subscription_links(
    item: &subscriptions::Model,
    site_context: &SiteContext,
) -> (Option<String>, Option<String>, Option<String>) {
    let manage_url = item.manage_token.as_deref().and_then(|token| {
        build_token_link(
            site_context.site_url.as_deref(),
            "/subscriptions/manage",
            token,
        )
    });
    let unsubscribe_url = item.manage_token.as_deref().and_then(|token| {
        build_token_link(
            site_context.site_url.as_deref(),
            "/subscriptions/unsubscribe",
            token,
        )
    });
    let confirm_url = item.confirm_token.as_deref().and_then(|token| {
        build_token_link(
            site_context.site_url.as_deref(),
            "/subscriptions/confirm",
            token,
        )
    });

    (manage_url, unsubscribe_url, confirm_url)
}

async fn send_confirmation_email(ctx: &AppContext, item: &subscriptions::Model) -> Result<()> {
    if item.channel_type != CHANNEL_EMAIL {
        return Ok(());
    }

    let site_context = load_site_context(ctx).await;
    let (manage_url, _, confirm_url) = subscription_links(item, &site_context);
    let confirm_url = confirm_url.ok_or_else(|| {
        Error::BadRequest("站点未配置 site_url，暂时无法生成订阅确认链接".to_string())
    })?;

    SubscriptionMailer::send_confirmation(
        ctx,
        &item.target,
        site_context.site_name.as_deref(),
        site_context.site_url.as_deref(),
        &confirm_url,
        manage_url.as_deref(),
    )
    .await
}

fn subscription_allows_event(
    item: &subscriptions::Model,
    event_type: &str,
    payload: &Value,
) -> bool {
    if normalize_status(&item.status) != STATUS_ACTIVE {
        return false;
    }

    if item.channel_type == CHANNEL_EMAIL
        && item
            .verified_at
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .is_none()
    {
        return false;
    }

    let normalized_event = normalize_string(event_type);
    let event_types = json_string_list(item.filters.as_ref(), "event_types");
    if !event_types.is_empty() && !event_types.iter().any(|item| item == &normalized_event) {
        return false;
    }

    let categories = json_string_list(item.filters.as_ref(), "categories");
    if !categories.is_empty() {
        let payload_categories = payload_match_strings(payload, "category");
        if payload_categories.is_empty()
            || !categories
                .iter()
                .any(|category| payload_categories.iter().any(|item| item == category))
        {
            return false;
        }
    }

    let tags = json_string_list(item.filters.as_ref(), "tags");
    if !tags.is_empty() {
        let payload_tags = payload_match_strings(payload, "tags");
        if payload_tags.is_empty()
            || !tags
                .iter()
                .any(|tag| payload_tags.iter().any(|item| item == tag))
        {
            return false;
        }
    }

    true
}

pub async fn list_subscriptions(
    ctx: &AppContext,
    channel_type: Option<&str>,
    status: Option<&str>,
) -> Result<Vec<subscriptions::Model>> {
    let mut query =
        subscriptions::Entity::find().order_by(subscriptions::Column::CreatedAt, Order::Desc);

    if let Some(channel_type) = channel_type
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        query = query
            .filter(subscriptions::Column::ChannelType.eq(normalize_channel_type(channel_type)));
    }

    if let Some(status) = status.map(str::trim).filter(|value| !value.is_empty()) {
        query = query.filter(subscriptions::Column::Status.eq(normalize_status(status)));
    }

    query.all(&ctx.db).await.map_err(Into::into)
}

pub async fn list_recent_deliveries(
    ctx: &AppContext,
    limit: u64,
) -> Result<Vec<notification_deliveries::Model>> {
    notification_deliveries::Entity::find()
        .order_by(notification_deliveries::Column::CreatedAt, Order::Desc)
        .limit(limit)
        .all(&ctx.db)
        .await
        .map_err(Into::into)
}

pub async fn create_public_email_subscription(
    ctx: &AppContext,
    email: &str,
    display_name: Option<String>,
    metadata: Option<Value>,
) -> Result<PublicSubscriptionResult> {
    let normalized_email = normalize_string(email);
    if normalized_email.is_empty() || !normalized_email.contains('@') {
        return Err(Error::BadRequest("请输入有效邮箱地址".to_string()));
    }

    let existing = subscriptions::Entity::find()
        .filter(subscriptions::Column::ChannelType.eq(CHANNEL_EMAIL))
        .filter(subscriptions::Column::Target.eq(&normalized_email))
        .one(&ctx.db)
        .await?;

    let display_name = trim_to_option(display_name);

    if let Some(existing) = existing {
        let mut active = existing.clone().into_active_model();
        let manage_token = existing
            .manage_token
            .clone()
            .filter(|value| !value.trim().is_empty())
            .unwrap_or_else(generate_subscription_token);
        active.manage_token = Set(Some(manage_token));

        if display_name.is_some() {
            active.display_name = Set(display_name);
        }
        active.metadata = Set(merge_metadata(existing.metadata.as_ref(), metadata));

        let normalized_status = normalize_status(&existing.status);
        if existing.verified_at.is_some() && normalized_status != STATUS_UNSUBSCRIBED {
            if normalized_status != STATUS_ACTIVE {
                active.status = Set(STATUS_ACTIVE.to_string());
            }
            active.confirm_token = Set(None);
            let updated = active.update(&ctx.db).await?;
            let message = if normalized_status == STATUS_PAUSED {
                "该邮箱已重新启用订阅。".to_string()
            } else {
                "该邮箱已经订阅，可直接使用邮件中的管理链接调整偏好。".to_string()
            };
            return Ok(PublicSubscriptionResult {
                subscription: to_public_subscription_view(&updated),
                requires_confirmation: false,
                message,
            });
        }

        active.status = Set(STATUS_PENDING.to_string());
        active.confirm_token = Set(Some(generate_subscription_token()));
        active.verified_at = Set(None);
        let updated = active.update(&ctx.db).await?;
        send_confirmation_email(ctx, &updated).await?;
        return Ok(PublicSubscriptionResult {
            subscription: to_public_subscription_view(&updated),
            requires_confirmation: true,
            message: "订阅申请已收到，请前往邮箱点击确认链接后生效。".to_string(),
        });
    }

    let created = subscriptions::ActiveModel {
        channel_type: Set(CHANNEL_EMAIL.to_string()),
        target: Set(normalized_email),
        display_name: Set(display_name),
        status: Set(STATUS_PENDING.to_string()),
        filters: Set(Some(default_public_filters())),
        secret: Set(None),
        notes: Set(None),
        confirm_token: Set(Some(generate_subscription_token())),
        manage_token: Set(Some(generate_subscription_token())),
        metadata: Set(metadata),
        verified_at: Set(None),
        last_notified_at: Set(None),
        failure_count: Set(Some(0)),
        last_delivery_status: Set(None),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;

    send_confirmation_email(ctx, &created).await?;

    Ok(PublicSubscriptionResult {
        subscription: to_public_subscription_view(&created),
        requires_confirmation: true,
        message: "订阅申请已收到，请前往邮箱点击确认链接后生效。".to_string(),
    })
}

pub async fn create_public_web_push_subscription(
    ctx: &AppContext,
    subscription: Value,
    metadata: Option<Value>,
) -> Result<PublicSubscriptionResult> {
    let normalized_subscription = normalize_browser_push_subscription(subscription)?;
    let endpoint = normalized_subscription
        .get("endpoint")
        .and_then(Value::as_str)
        .ok_or_else(|| Error::BadRequest("browser push endpoint 非法".to_string()))?
        .to_string();

    let existing = subscriptions::Entity::find()
        .filter(subscriptions::Column::ChannelType.eq(CHANNEL_WEB_PUSH))
        .filter(subscriptions::Column::Target.eq(&endpoint))
        .one(&ctx.db)
        .await?;

    if let Some(existing) = existing {
        let mut active = existing.clone().into_active_model();
        let manage_token = existing
            .manage_token
            .clone()
            .filter(|value| !value.trim().is_empty())
            .unwrap_or_else(generate_subscription_token);
        active.manage_token = Set(Some(manage_token));
        active.status = Set(STATUS_ACTIVE.to_string());
        active.confirm_token = Set(None);
        active.verified_at = Set(Some(Utc::now().to_rfc3339()));
        active.failure_count = Set(Some(0));
        active.last_delivery_status = Set(None);
        active.notes = Set(remove_subscription_note(
            existing.notes.as_deref(),
            WEB_PUSH_AUTO_PAUSE_NOTE,
        ));
        active.metadata = Set(Some(merge_browser_push_metadata(
            existing.metadata.as_ref(),
            metadata,
            normalized_subscription,
        )));
        if existing
            .display_name
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .is_none()
        {
            active.display_name = Set(Some("Browser Push".to_string()));
        }
        let updated = active.update(&ctx.db).await?;
        return Ok(PublicSubscriptionResult {
            subscription: to_public_subscription_view(&updated),
            requires_confirmation: false,
            message: "浏览器推送已更新，后续有新内容时会直接提醒。".to_string(),
        });
    }

    let created = subscriptions::ActiveModel {
        channel_type: Set(CHANNEL_WEB_PUSH.to_string()),
        target: Set(endpoint),
        display_name: Set(Some("Browser Push".to_string())),
        status: Set(STATUS_ACTIVE.to_string()),
        filters: Set(Some(default_public_filters())),
        secret: Set(None),
        notes: Set(None),
        confirm_token: Set(None),
        manage_token: Set(Some(generate_subscription_token())),
        metadata: Set(Some(merge_browser_push_metadata(
            None,
            metadata,
            normalized_subscription,
        ))),
        verified_at: Set(Some(Utc::now().to_rfc3339())),
        last_notified_at: Set(None),
        failure_count: Set(Some(0)),
        last_delivery_status: Set(None),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;

    Ok(PublicSubscriptionResult {
        subscription: to_public_subscription_view(&created),
        requires_confirmation: false,
        message: "浏览器推送已开启，后续有新内容时会直接提醒。".to_string(),
    })
}

pub async fn confirm_subscription(ctx: &AppContext, token: &str) -> Result<subscriptions::Model> {
    let token = token.trim();
    if token.is_empty() {
        return Err(Error::BadRequest("确认令牌不能为空".to_string()));
    }

    let item = subscriptions::Entity::find()
        .filter(subscriptions::Column::ConfirmToken.eq(token))
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::BadRequest("确认链接无效或已过期".to_string()))?;

    if normalize_status(&item.status) == STATUS_ACTIVE
        && item
            .verified_at
            .as_deref()
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .is_some()
    {
        return Ok(item);
    }

    let mut active = item.clone().into_active_model();
    active.status = Set(STATUS_ACTIVE.to_string());
    if item
        .verified_at
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .is_none()
    {
        active.verified_at = Set(Some(Utc::now().to_rfc3339()));
    }
    active.update(&ctx.db).await.map_err(Into::into)
}

pub async fn get_subscription_by_manage_token(
    ctx: &AppContext,
    token: &str,
) -> Result<subscriptions::Model> {
    let token = token.trim();
    if token.is_empty() {
        return Err(Error::BadRequest("管理令牌不能为空".to_string()));
    }

    subscriptions::Entity::find()
        .filter(subscriptions::Column::ManageToken.eq(token))
        .one(&ctx.db)
        .await?
        .ok_or_else(|| Error::BadRequest("管理链接无效或已过期".to_string()))
}

pub async fn update_subscription_preferences(
    ctx: &AppContext,
    token: &str,
    display_name: Option<String>,
    status: Option<String>,
    filters: Option<Value>,
) -> Result<subscriptions::Model> {
    let item = get_subscription_by_manage_token(ctx, token).await?;
    let mut active = item.into_active_model();

    if display_name.is_some() {
        active.display_name = Set(trim_to_option(display_name));
    }

    if let Some(status) = status {
        let normalized = normalize_status(&status);
        if normalized == STATUS_PENDING {
            return Err(Error::BadRequest(
                "偏好页不支持将状态改回 pending".to_string(),
            ));
        }
        active.status = Set(normalized);
    }

    if filters.is_some() {
        active.filters = Set(normalize_filters(filters));
    }

    active.update(&ctx.db).await.map_err(Into::into)
}

pub async fn unsubscribe_subscription(
    ctx: &AppContext,
    token: &str,
) -> Result<subscriptions::Model> {
    let item = get_subscription_by_manage_token(ctx, token).await?;
    let mut active = item.into_active_model();
    active.status = Set(STATUS_UNSUBSCRIBED.to_string());
    active.confirm_token = Set(None);
    active.update(&ctx.db).await.map_err(Into::into)
}

async fn active_subscriptions(ctx: &AppContext) -> Result<Vec<subscriptions::Model>> {
    subscriptions::Entity::find()
        .filter(subscriptions::Column::Status.eq(STATUS_ACTIVE))
        .order_by(subscriptions::Column::CreatedAt, Order::Asc)
        .all(&ctx.db)
        .await
        .map_err(Into::into)
}

async fn update_subscription_delivery_state(
    ctx: &AppContext,
    subscription_id: Option<i32>,
    status: &str,
    success: bool,
) -> Result<()> {
    let Some(subscription_id) = subscription_id else {
        return Ok(());
    };

    let Some(subscription) = subscriptions::Entity::find_by_id(subscription_id)
        .one(&ctx.db)
        .await?
    else {
        return Ok(());
    };

    let current_failures = subscription.failure_count.unwrap_or(0);
    let mut active = subscription.into_active_model();
    active.last_notified_at = Set(Some(Utc::now().to_rfc3339()));
    active.last_delivery_status = Set(Some(status.to_string()));
    active.failure_count = Set(Some(if success { 0 } else { current_failures + 1 }));
    let _ = active.update(&ctx.db).await?;
    Ok(())
}

async fn enqueue_delivery(ctx: &AppContext, delivery_id: i32) -> Result<()> {
    let _ = worker_jobs::queue_notification_delivery_job(
        ctx,
        delivery_id,
        None,
        Some("system".to_string()),
        None,
        Some("system".to_string()),
    )
    .await?;
    Ok(())
}

pub async fn queue_direct_notification(
    ctx: &AppContext,
    channel_type: &str,
    target: &str,
    event_type: &str,
    subject: &str,
    text: &str,
    payload: Value,
    site_name: Option<String>,
    site_url: Option<String>,
) -> Result<notification_deliveries::Model> {
    let channel_type = normalize_channel_type(channel_type);
    let target = target.trim().to_string();
    if target.is_empty() {
        return Err(Error::BadRequest("target 不能为空".to_string()));
    }

    let delivery = notification_deliveries::ActiveModel {
        subscription_id: Set(None),
        channel_type: Set(channel_type),
        target: Set(target),
        event_type: Set(event_type.to_string()),
        status: Set(DELIVERY_STATUS_QUEUED.to_string()),
        provider: Set(None),
        response_text: Set(None),
        payload: Set(Some(serde_json::to_value(QueuedDeliveryPayload {
            subject: subject.to_string(),
            headline: subject.to_string(),
            text: text.to_string(),
            site_name,
            site_url,
            payload,
            manage_url: None,
            unsubscribe_url: None,
        })?)),
        attempts_count: Set(0),
        next_retry_at: Set(None),
        last_attempt_at: Set(None),
        delivered_at: Set(None),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;

    enqueue_delivery(ctx, delivery.id).await?;
    Ok(delivery)
}

async fn queue_notification_for_subscription(
    ctx: &AppContext,
    item: &subscriptions::Model,
    event_type: &str,
    subject: &str,
    text: &str,
    payload: Value,
    site_context: &SiteContext,
) -> Result<notification_deliveries::Model> {
    let item = ensure_subscription_tokens(ctx, item).await?;
    let (manage_url, unsubscribe_url, _) = subscription_links(&item, site_context);

    let delivery = notification_deliveries::ActiveModel {
        subscription_id: Set(Some(item.id)),
        channel_type: Set(item.channel_type.clone()),
        target: Set(item.target.clone()),
        event_type: Set(event_type.to_string()),
        status: Set(DELIVERY_STATUS_QUEUED.to_string()),
        provider: Set(None),
        response_text: Set(None),
        payload: Set(Some(serde_json::to_value(QueuedDeliveryPayload {
            subject: subject.to_string(),
            headline: subject.to_string(),
            text: text.to_string(),
            site_name: site_context.site_name.clone(),
            site_url: site_context.site_url.clone(),
            payload,
            manage_url,
            unsubscribe_url,
        })?)),
        attempts_count: Set(0),
        next_retry_at: Set(None),
        last_attempt_at: Set(None),
        delivered_at: Set(None),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;

    enqueue_delivery(ctx, delivery.id).await?;
    Ok(delivery)
}

pub async fn queue_event_for_active_subscriptions(
    ctx: &AppContext,
    event_type: &str,
    subject: &str,
    text: &str,
    payload: Value,
    site_name: Option<String>,
    site_url: Option<String>,
) -> Result<QueueDispatchSummary> {
    let subscriptions = active_subscriptions(ctx).await?;
    if subscriptions.is_empty() {
        return Ok(QueueDispatchSummary {
            queued: 0,
            skipped: 0,
        });
    }

    let site_context = SiteContext {
        site_name,
        site_url,
    };
    let mut queued = 0usize;
    let mut skipped = 0usize;

    for item in subscriptions {
        if !subscription_allows_event(&item, event_type, &payload) {
            skipped += 1;
            continue;
        }

        queue_notification_for_subscription(
            ctx,
            &item,
            event_type,
            subject,
            text,
            payload.clone(),
            &site_context,
        )
        .await?;
        queued += 1;
    }

    Ok(QueueDispatchSummary { queued, skipped })
}

fn provider_name(channel_type: &str) -> &'static str {
    match channel_type {
        CHANNEL_EMAIL => "smtp",
        CHANNEL_DISCORD => "discord-webhook",
        CHANNEL_TELEGRAM => "telegram-bot-api",
        CHANNEL_NTFY => "ntfy",
        CHANNEL_WEB_PUSH => "web-push",
        _ => "webhook",
    }
}

fn resolve_ntfy_target(target: &str) -> String {
    let trimmed = target.trim();
    if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
        trimmed.to_string()
    } else {
        format!("https://ntfy.sh/{trimmed}")
    }
}

fn collapse_whitespace(value: &str) -> String {
    value.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn truncate_chars(value: &str, max_chars: usize) -> String {
    if value.chars().count() <= max_chars {
        return value.to_string();
    }

    let mut sliced = value.chars().take(max_chars).collect::<String>();
    sliced.push('…');
    sliced
}

fn site_asset_url(site_url: Option<&str>, path: &str) -> Option<String> {
    let base = site_url?.trim().trim_end_matches('/');
    if base.is_empty() {
        return None;
    }

    Some(format!("{base}{path}"))
}

fn web_push_target_url(message: &QueuedDeliveryPayload) -> Option<String> {
    message
        .payload
        .get("url")
        .and_then(Value::as_str)
        .map(ToString::to_string)
        .or_else(|| message.site_url.clone())
}

fn build_web_push_payload(message: &QueuedDeliveryPayload) -> Value {
    let title = truncate_chars(
        &collapse_whitespace(&message.subject),
        WEB_PUSH_TITLE_MAX_CHARS,
    );
    let body = truncate_chars(&collapse_whitespace(&message.text), WEB_PUSH_BODY_MAX_CHARS);
    let url = web_push_target_url(message);
    let event_type = message
        .payload
        .get("event_type")
        .and_then(Value::as_str)
        .unwrap_or("subscription");

    serde_json::json!({
        "title": title,
        "body": body,
        "icon": site_asset_url(message.site_url.as_deref(), "/favicon.svg"),
        "badge": site_asset_url(message.site_url.as_deref(), "/favicon.ico"),
        "url": url.clone(),
        "tag": event_type,
        "data": {
            "url": url,
            "event_type": event_type,
        }
    })
}

fn encode_web_push_payload(message: &QueuedDeliveryPayload) -> Result<Vec<u8>> {
    let payload = build_web_push_payload(message);
    let encoded = serde_json::to_vec(&payload)?;

    if encoded.len() > WEB_PUSH_MAX_PAYLOAD_BYTES {
        return Err(Error::BadRequest(format!(
            "web push payload too large: {} bytes exceeds safe limit {} bytes",
            encoded.len(),
            WEB_PUSH_MAX_PAYLOAD_BYTES
        )));
    }

    Ok(encoded)
}

async fn deliver_via_channel(
    ctx: &AppContext,
    channel_type: &str,
    target: &str,
    message: &QueuedDeliveryPayload,
    metadata: Option<&Value>,
) -> Result<Option<String>> {
    match channel_type {
        CHANNEL_EMAIL => Err(Error::BadRequest(
            "email channel must be delivered via subscription context".to_string(),
        )),
        CHANNEL_DISCORD => Client::new()
            .post(target)
            .json(&serde_json::json!({ "content": message.text }))
            .send()
            .await
            .and_then(|response| response.error_for_status())
            .map(|_| None)
            .map_err(|error| Error::BadRequest(error.to_string())),
        CHANNEL_TELEGRAM => Client::new()
            .post(target)
            .json(&serde_json::json!({ "text": message.text }))
            .send()
            .await
            .and_then(|response| response.error_for_status())
            .map(|_| None)
            .map_err(|error| Error::BadRequest(error.to_string())),
        CHANNEL_NTFY => Client::new()
            .post(resolve_ntfy_target(target))
            .header("Title", &message.subject)
            .header("Content-Type", "text/plain; charset=utf-8")
            .body(message.text.clone())
            .send()
            .await
            .and_then(|response| response.error_for_status())
            .map(|_| None)
            .map_err(|error| Error::BadRequest(error.to_string())),
        CHANNEL_WEB_PUSH => {
            let settings = crate::controllers::site_settings::load_current(ctx).await?;
            let subscription_info = web_push_service::subscription_info_from_metadata(metadata)?;
            let payload = encode_web_push_payload(message)?;
            web_push_service::send_payload(
                &settings,
                &subscription_info,
                &payload,
                Some(web_push::Urgency::Normal),
                24 * 60 * 60,
                message.site_url.as_deref(),
            )
            .await?;
            Ok(None)
        }
        _ => {
            let envelope = DeliveryEnvelope {
                event: message
                    .payload
                    .get("event_type")
                    .and_then(Value::as_str)
                    .map(ToString::to_string)
                    .unwrap_or_default(),
                site_name: message.site_name.clone(),
                site_url: message.site_url.clone(),
                timestamp: Utc::now().to_rfc3339(),
                subject: message.subject.clone(),
                text: message.text.clone(),
                data: message.payload.clone(),
            };

            Client::new()
                .post(target)
                .json(&envelope)
                .send()
                .await
                .and_then(|response| response.error_for_status())
                .map(|_| None)
                .map_err(|error| Error::BadRequest(error.to_string()))
        }
    }
}

pub async fn process_delivery(ctx: &AppContext, delivery_id: i32) -> Result<()> {
    let Some(delivery) = notification_deliveries::Entity::find_by_id(delivery_id)
        .one(&ctx.db)
        .await?
    else {
        return Ok(());
    };

    if matches!(
        delivery.status.as_str(),
        DELIVERY_STATUS_SENT | DELIVERY_STATUS_SKIPPED | DELIVERY_STATUS_EXHAUSTED
    ) {
        return Ok(());
    }

    let message = delivery
        .payload
        .clone()
        .ok_or_else(|| Error::BadRequest("delivery payload 为空".to_string()))
        .and_then(|value| {
            serde_json::from_value::<QueuedDeliveryPayload>(value).map_err(Into::into)
        })?;

    let attempts = delivery.attempts_count + 1;
    let now = Utc::now().to_rfc3339();

    let subscription = match delivery.subscription_id {
        Some(subscription_id) => {
            subscriptions::Entity::find_by_id(subscription_id)
                .one(&ctx.db)
                .await?
        }
        None => None,
    };

    if let Some(subscription) = subscription.as_ref() {
        if normalize_status(&subscription.status) != STATUS_ACTIVE {
            let mut active = delivery.into_active_model();
            active.status = Set(DELIVERY_STATUS_SKIPPED.to_string());
            active.response_text = Set(Some("subscription is not active".to_string()));
            active.attempts_count = Set(attempts);
            active.last_attempt_at = Set(Some(now));
            active.next_retry_at = Set(None);
            active.delivered_at = Set(Some(Utc::now().to_rfc3339()));
            let _ = active.update(&ctx.db).await?;
            update_subscription_delivery_state(
                ctx,
                Some(subscription.id),
                DELIVERY_STATUS_SKIPPED,
                false,
            )
            .await?;
            return Ok(());
        }
    }

    let send_result = if let Some(subscription) = subscription.as_ref() {
        if subscription.channel_type == CHANNEL_EMAIL {
            SubscriptionMailer::send_notification(
                ctx,
                &subscription.target,
                &message.subject,
                &message.headline,
                &message.text,
                message.site_name.as_deref(),
                message.site_url.as_deref(),
                message.manage_url.as_deref(),
                message.unsubscribe_url.as_deref(),
            )
            .await
            .map(|_| None)
        } else {
            deliver_via_channel(
                ctx,
                &subscription.channel_type,
                &subscription.target,
                &message,
                subscription.metadata.as_ref(),
            )
            .await
        }
    } else {
        deliver_via_channel(
            ctx,
            &delivery.channel_type,
            &delivery.target,
            &message,
            None,
        )
        .await
    };
    let subscription_id = delivery.subscription_id;
    let delivery_channel_type = delivery.channel_type.clone();

    match send_result {
        Ok(response_text) => {
            let mut active = delivery.into_active_model();
            active.status = Set(DELIVERY_STATUS_SENT.to_string());
            active.provider = Set(Some(provider_name(&delivery_channel_type).to_string()));
            active.response_text = Set(response_text);
            active.attempts_count = Set(attempts);
            active.last_attempt_at = Set(Some(Utc::now().to_rfc3339()));
            active.next_retry_at = Set(None);
            active.delivered_at = Set(Some(Utc::now().to_rfc3339()));
            let _ = active.update(&ctx.db).await?;
            update_subscription_delivery_state(ctx, subscription_id, DELIVERY_STATUS_SENT, true)
                .await?;
        }
        Err(error) => {
            let error_text = error.to_string();
            let mut active = delivery.into_active_model();
            active.status = Set(DELIVERY_STATUS_EXHAUSTED.to_string());
            active.provider = Set(Some(provider_name(&delivery_channel_type).to_string()));
            active.response_text = Set(Some(error_text.clone()));
            active.attempts_count = Set(attempts);
            active.last_attempt_at = Set(Some(Utc::now().to_rfc3339()));
            active.next_retry_at = Set(None);
            active.delivered_at = Set(Some(Utc::now().to_rfc3339()));
            let _ = active.update(&ctx.db).await?;
            update_subscription_delivery_state(
                ctx,
                subscription_id,
                DELIVERY_STATUS_EXHAUSTED,
                false,
            )
            .await?;
            maybe_pause_failed_web_push_subscription(ctx, subscription.as_ref(), &error_text)
                .await?;
            Err(error)?;
        }
    }

    Ok(())
}

pub async fn retry_due_deliveries(ctx: &AppContext, limit: u64) -> Result<usize> {
    let now = Utc::now().to_rfc3339();
    let items = notification_deliveries::Entity::find()
        .filter(notification_deliveries::Column::Status.eq(DELIVERY_STATUS_RETRY_PENDING))
        .filter(notification_deliveries::Column::NextRetryAt.lte(now))
        .order_by(notification_deliveries::Column::CreatedAt, Order::Asc)
        .limit(limit)
        .all(&ctx.db)
        .await?;

    let mut queued = 0usize;
    for item in items {
        enqueue_delivery(ctx, item.id).await?;
        queued += 1;
    }

    Ok(queued)
}

pub async fn send_test_notification(
    ctx: &AppContext,
    item: &subscriptions::Model,
) -> Result<notification_deliveries::Model> {
    let site_context = load_site_context(ctx).await;
    let text = format!(
        "这是一条来自 {} 的测试通知。\n\n频道：{}\n目标：{}\n时间：{}",
        site_context
            .site_name
            .clone()
            .unwrap_or_else(|| "Termi".to_string()),
        item.channel_type,
        item.target,
        Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
    );

    queue_notification_for_subscription(
        ctx,
        item,
        EVENT_SUBSCRIPTION_TEST,
        "Termi 订阅测试通知",
        &text,
        serde_json::json!({
            "event_type": EVENT_SUBSCRIPTION_TEST,
            "kind": "test",
            "channel": item.channel_type,
            "target": item.target,
        }),
        &site_context,
    )
    .await
}

pub async fn notify_post_published(
    ctx: &AppContext,
    post: &content::MarkdownPost,
) -> Result<QueueDispatchSummary> {
    let site_context = load_site_context(ctx).await;
    let public_url = post_public_url(site_context.site_url.as_deref(), &post.slug);
    let subject = format!("新文章发布：{}", post.title);
    let payload = serde_json::json!({
        "event_type": EVENT_POST_PUBLISHED,
        "slug": post.slug,
        "title": post.title,
        "description": post.description,
        "category": post.category,
        "tags": post.tags,
        "url": public_url,
        "published_at": Utc::now().to_rfc3339(),
    });
    let text = format!(
        "《{}》已发布。\n\n分类：{}\n标签：{}\n链接：{}\n\n{}",
        post.title,
        post.category
            .clone()
            .unwrap_or_else(|| "未分类".to_string()),
        if post.tags.is_empty() {
            "无".to_string()
        } else {
            post.tags.join(", ")
        },
        public_url
            .clone()
            .unwrap_or_else(|| format!("/articles/{}", post.slug)),
        post.description.clone().unwrap_or_default(),
    );

    queue_event_for_active_subscriptions(
        ctx,
        EVENT_POST_PUBLISHED,
        &subject,
        &text,
        payload,
        site_context.site_name,
        site_context.site_url,
    )
    .await
}

pub async fn send_digest(ctx: &AppContext, period: &str) -> Result<DigestDispatchSummary> {
    let (normalized_period, days, event_type) = effective_period(period);
    let since = Utc::now() - Duration::days(days);
    let posts = posts::Entity::find()
        .order_by(posts::Column::CreatedAt, Order::Desc)
        .all(&ctx.db)
        .await?
        .into_iter()
        .filter(|post| {
            post.created_at.with_timezone(&Utc) >= since
                && content::is_post_listed_publicly(post, Utc::now().fixed_offset())
        })
        .collect::<Vec<_>>();

    let site_context = load_site_context(ctx).await;
    let lines = if posts.is_empty() {
        vec![format!("最近 {} 天还没有新的公开文章。", days)]
    } else {
        posts
            .iter()
            .map(|post| {
                let url = post_public_url(site_context.site_url.as_deref(), &post.slug)
                    .unwrap_or_else(|| format!("/articles/{}", post.slug));
                format!(
                    "- {}\n  {}\n  {}",
                    post.title.clone().unwrap_or_else(|| post.slug.clone()),
                    url,
                    post.description.clone().unwrap_or_default()
                )
            })
            .collect::<Vec<_>>()
    };

    let subject = format!(
        "{} 内容摘要",
        if normalized_period == "monthly" {
            "月报"
        } else {
            "周报"
        }
    );
    let body = format!("统计周期：最近 {} 天\n\n{}", days, lines.join("\n\n"));
    let payload = serde_json::json!({
        "event_type": event_type,
        "period": normalized_period,
        "days": days,
        "post_count": posts.len(),
        "posts": posts.iter().map(|post| serde_json::json!({
            "slug": post.slug,
            "title": post.title,
            "description": post.description,
            "category": post.category,
            "tags": post.tags,
        })).collect::<Vec<_>>(),
    });

    let summary = queue_event_for_active_subscriptions(
        ctx,
        event_type,
        &subject,
        &body,
        payload,
        site_context.site_name,
        site_context.site_url,
    )
    .await?;

    Ok(DigestDispatchSummary {
        period: normalized_period.to_string(),
        post_count: posts.len(),
        queued: summary.queued,
        skipped: summary.skipped,
    })
}
