#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]

use axum::http::HeaderMap;
use loco_rs::prelude::*;
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, QueryOrder, Set};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use uuid::Uuid;

use crate::{
    controllers::admin::check_auth,
    models::_entities::{
        categories, friend_links, posts,
        site_settings::{self, ActiveModel, Entity, Model},
        tags,
    },
    services::{ai, content},
};

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct MusicTrackPayload {
    pub title: String,
    #[serde(default)]
    pub artist: Option<String>,
    #[serde(default)]
    pub album: Option<String>,
    pub url: String,
    #[serde(default, alias = "coverImageUrl")]
    pub cover_image_url: Option<String>,
    #[serde(default, alias = "accentColor")]
    pub accent_color: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AiProviderConfig {
    #[serde(default)]
    pub id: String,
    #[serde(default, alias = "label")]
    pub name: String,
    #[serde(default)]
    pub provider: String,
    #[serde(default, alias = "apiBase")]
    pub api_base: Option<String>,
    #[serde(default, alias = "apiKey")]
    pub api_key: Option<String>,
    #[serde(default, alias = "chatModel")]
    pub chat_model: Option<String>,
    #[serde(default, alias = "imageModel")]
    pub image_model: Option<String>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SiteSettingsPayload {
    #[serde(default, alias = "siteName")]
    pub site_name: Option<String>,
    #[serde(default, alias = "siteShortName")]
    pub site_short_name: Option<String>,
    #[serde(default, alias = "siteUrl")]
    pub site_url: Option<String>,
    #[serde(default, alias = "siteTitle")]
    pub site_title: Option<String>,
    #[serde(default, alias = "siteDescription")]
    pub site_description: Option<String>,
    #[serde(default, alias = "heroTitle")]
    pub hero_title: Option<String>,
    #[serde(default, alias = "heroSubtitle")]
    pub hero_subtitle: Option<String>,
    #[serde(default, alias = "ownerName")]
    pub owner_name: Option<String>,
    #[serde(default, alias = "ownerTitle")]
    pub owner_title: Option<String>,
    #[serde(default, alias = "ownerBio")]
    pub owner_bio: Option<String>,
    #[serde(default, alias = "ownerAvatarUrl")]
    pub owner_avatar_url: Option<String>,
    #[serde(default, alias = "socialGithub")]
    pub social_github: Option<String>,
    #[serde(default, alias = "socialTwitter")]
    pub social_twitter: Option<String>,
    #[serde(default, alias = "socialEmail")]
    pub social_email: Option<String>,
    #[serde(default)]
    pub location: Option<String>,
    #[serde(default, alias = "techStack")]
    pub tech_stack: Option<Vec<String>>,
    #[serde(default, alias = "musicPlaylist")]
    pub music_playlist: Option<Vec<MusicTrackPayload>>,
    #[serde(default, alias = "musicEnabled")]
    pub music_enabled: Option<bool>,
    #[serde(default, alias = "maintenanceModeEnabled")]
    pub maintenance_mode_enabled: Option<bool>,
    #[serde(default, alias = "maintenanceAccessCode")]
    pub maintenance_access_code: Option<String>,
    #[serde(default, alias = "aiEnabled")]
    pub ai_enabled: Option<bool>,
    #[serde(default, alias = "paragraphCommentsEnabled")]
    pub paragraph_comments_enabled: Option<bool>,
    #[serde(default, alias = "commentVerificationMode")]
    pub comment_verification_mode: Option<String>,
    #[serde(default, alias = "commentTurnstileEnabled")]
    pub comment_turnstile_enabled: Option<bool>,
    #[serde(default, alias = "subscriptionVerificationMode")]
    pub subscription_verification_mode: Option<String>,
    #[serde(default, alias = "subscriptionTurnstileEnabled")]
    pub subscription_turnstile_enabled: Option<bool>,
    #[serde(default, alias = "webPushEnabled")]
    pub web_push_enabled: Option<bool>,
    #[serde(default, alias = "turnstileSiteKey")]
    pub turnstile_site_key: Option<String>,
    #[serde(default, alias = "turnstileSecretKey")]
    pub turnstile_secret_key: Option<String>,
    #[serde(default, alias = "webPushVapidPublicKey")]
    pub web_push_vapid_public_key: Option<String>,
    #[serde(default, alias = "webPushVapidPrivateKey")]
    pub web_push_vapid_private_key: Option<String>,
    #[serde(default, alias = "webPushVapidSubject")]
    pub web_push_vapid_subject: Option<String>,
    #[serde(default, alias = "aiProvider")]
    pub ai_provider: Option<String>,
    #[serde(default, alias = "aiApiBase")]
    pub ai_api_base: Option<String>,
    #[serde(default, alias = "aiApiKey")]
    pub ai_api_key: Option<String>,
    #[serde(default, alias = "aiChatModel")]
    pub ai_chat_model: Option<String>,
    #[serde(default, alias = "aiImageProvider")]
    pub ai_image_provider: Option<String>,
    #[serde(default, alias = "aiImageApiBase")]
    pub ai_image_api_base: Option<String>,
    #[serde(default, alias = "aiImageApiKey")]
    pub ai_image_api_key: Option<String>,
    #[serde(default, alias = "aiImageModel")]
    pub ai_image_model: Option<String>,
    #[serde(default, alias = "aiProviders")]
    pub ai_providers: Option<Vec<AiProviderConfig>>,
    #[serde(default, alias = "aiActiveProviderId")]
    pub ai_active_provider_id: Option<String>,
    #[serde(default, alias = "aiEmbeddingModel")]
    pub ai_embedding_model: Option<String>,
    #[serde(default, alias = "aiSystemPrompt")]
    pub ai_system_prompt: Option<String>,
    #[serde(default, alias = "aiTopK")]
    pub ai_top_k: Option<i32>,
    #[serde(default, alias = "aiChunkSize")]
    pub ai_chunk_size: Option<i32>,
    #[serde(default, alias = "mediaR2AccountId")]
    pub media_r2_account_id: Option<String>,
    #[serde(default, alias = "mediaStorageProvider")]
    pub media_storage_provider: Option<String>,
    #[serde(default, alias = "mediaR2Bucket")]
    pub media_r2_bucket: Option<String>,
    #[serde(default, alias = "mediaR2PublicBaseUrl")]
    pub media_r2_public_base_url: Option<String>,
    #[serde(default, alias = "mediaR2AccessKeyId")]
    pub media_r2_access_key_id: Option<String>,
    #[serde(default, alias = "mediaR2SecretAccessKey")]
    pub media_r2_secret_access_key: Option<String>,
    #[serde(default, alias = "seoFaviconUrl")]
    pub seo_favicon_url: Option<String>,
    #[serde(default, alias = "seoDefaultOgImage")]
    pub seo_default_og_image: Option<String>,
    #[serde(default, alias = "seoDefaultTwitterHandle")]
    pub seo_default_twitter_handle: Option<String>,
    #[serde(default, alias = "seoWechatShareQrEnabled")]
    pub seo_wechat_share_qr_enabled: Option<bool>,
    #[serde(default, alias = "notificationWebhookUrl")]
    pub notification_webhook_url: Option<String>,
    #[serde(default, alias = "notificationChannelType")]
    pub notification_channel_type: Option<String>,
    #[serde(default, alias = "notificationCommentEnabled")]
    pub notification_comment_enabled: Option<bool>,
    #[serde(default, alias = "notificationFriendLinkEnabled")]
    pub notification_friend_link_enabled: Option<bool>,
    #[serde(default, alias = "subscriptionPopupEnabled")]
    pub subscription_popup_enabled: Option<bool>,
    #[serde(default, alias = "subscriptionPopupTitle")]
    pub subscription_popup_title: Option<String>,
    #[serde(default, alias = "subscriptionPopupDescription")]
    pub subscription_popup_description: Option<String>,
    #[serde(default, alias = "subscriptionPopupDelaySeconds")]
    pub subscription_popup_delay_seconds: Option<i32>,
    #[serde(default, alias = "searchSynonyms")]
    pub search_synonyms: Option<Vec<String>>,
}

#[derive(Clone, Debug, Serialize)]
pub struct PublicSiteSettingsResponse {
    pub id: i32,
    pub site_name: Option<String>,
    pub site_short_name: Option<String>,
    pub site_url: Option<String>,
    pub site_title: Option<String>,
    pub site_description: Option<String>,
    pub hero_title: Option<String>,
    pub hero_subtitle: Option<String>,
    pub owner_name: Option<String>,
    pub owner_title: Option<String>,
    pub owner_bio: Option<String>,
    pub owner_avatar_url: Option<String>,
    pub social_github: Option<String>,
    pub social_twitter: Option<String>,
    pub social_email: Option<String>,
    pub location: Option<String>,
    pub tech_stack: Option<serde_json::Value>,
    pub music_playlist: Option<serde_json::Value>,
    pub music_enabled: bool,
    pub ai_enabled: bool,
    pub paragraph_comments_enabled: bool,
    pub comment_verification_mode: String,
    pub comment_turnstile_enabled: bool,
    pub subscription_verification_mode: String,
    pub subscription_turnstile_enabled: bool,
    pub web_push_enabled: bool,
    pub turnstile_site_key: Option<String>,
    pub web_push_vapid_public_key: Option<String>,
    pub subscription_popup_enabled: bool,
    pub subscription_popup_title: String,
    pub subscription_popup_description: String,
    pub subscription_popup_delay_seconds: i32,
    pub seo_favicon_url: Option<String>,
    pub seo_default_og_image: Option<String>,
    pub seo_default_twitter_handle: Option<String>,
    pub seo_wechat_share_qr_enabled: bool,
}

#[derive(Clone, Debug, Default, Deserialize)]
pub struct MaintenanceAccessTokenPayload {
    #[serde(default, alias = "accessToken")]
    pub access_token: Option<String>,
}

#[derive(Clone, Debug, Default, Deserialize)]
pub struct MaintenanceVerifyPayload {
    #[serde(default)]
    pub code: Option<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct MaintenanceAccessStatusResponse {
    pub maintenance_mode_enabled: bool,
    pub access_granted: bool,
}

#[derive(Clone, Debug, Serialize)]
pub struct MaintenanceVerifyResponse {
    pub maintenance_mode_enabled: bool,
    pub access_granted: bool,
    pub access_token: Option<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct HomeCategorySummary {
    pub id: i32,
    pub name: String,
    pub slug: String,
    pub count: usize,
}

#[derive(Clone, Debug, Serialize)]
pub struct HomePageResponse {
    pub site_settings: PublicSiteSettingsResponse,
    pub posts: Vec<posts::Model>,
    pub tags: Vec<tags::Model>,
    pub friend_links: Vec<friend_links::Model>,
    pub categories: Vec<HomeCategorySummary>,
    pub content_overview: crate::services::analytics::ContentAnalyticsOverview,
    pub popular_posts: Vec<crate::services::analytics::AnalyticsPopularPost>,
    pub content_ranges: Vec<crate::services::analytics::PublicContentWindowHighlights>,
}

fn normalize_optional_string(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_optional_int(value: Option<i32>, min: i32, max: i32) -> Option<i32> {
    value.map(|item| item.clamp(min, max))
}

fn maintenance_mode_enabled(model: &Model) -> bool {
    model.maintenance_mode_enabled.unwrap_or(false)
}

fn maintenance_access_code(model: &Model) -> Option<String> {
    normalize_optional_string(model.maintenance_access_code.clone())
}

fn maintenance_access_token_from_secret(secret: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(b"termi-maintenance-access:v1:");
    hasher.update(secret.as_bytes());
    let digest = hasher.finalize();

    digest
        .iter()
        .map(|byte| format!("{byte:02x}"))
        .collect::<String>()
}

fn validate_maintenance_access_token(model: &Model, token: Option<&str>) -> bool {
    let Some(candidate) = token.and_then(|item| {
        let trimmed = item.trim();
        (!trimmed.is_empty()).then(|| trimmed.to_string())
    }) else {
        return false;
    };

    let Some(secret) = maintenance_access_code(model) else {
        return false;
    };

    candidate == maintenance_access_token_from_secret(&secret)
}

fn verify_maintenance_access_code(model: &Model, code: Option<&str>) -> Option<String> {
    let candidate = code.and_then(|item| {
        let trimmed = item.trim();
        (!trimmed.is_empty()).then(|| trimmed.to_string())
    })?;
    let secret = maintenance_access_code(model)?;

    (candidate == secret).then(|| maintenance_access_token_from_secret(&secret))
}

fn normalize_notification_channel_type(value: Option<String>) -> Option<String> {
    value.and_then(|item| {
        let normalized = item.trim().to_ascii_lowercase();
        match normalized.as_str() {
            "ntfy" => Some("ntfy".to_string()),
            "webhook" => Some("webhook".to_string()),
            _ => None,
        }
    })
}

pub(crate) fn default_subscription_popup_enabled() -> bool {
    true
}

pub(crate) fn default_subscription_popup_title() -> String {
    "订阅更新".to_string()
}

pub(crate) fn default_subscription_popup_description() -> String {
    "有新内容时及时提醒你；如果愿意，也可以再留一个邮箱备份。".to_string()
}

pub(crate) fn default_subscription_popup_delay_seconds() -> i32 {
    18
}

const DEFAULT_TURNSTILE_SITE_KEY: &str = "0x4AAAAAACy58kMBSwXwqMhx";
const DEFAULT_TURNSTILE_SECRET_KEY: &str = "0x4AAAAAACy58m3gYfSqM-VIz4QK4wuO73U";

fn normalize_string_list(values: Vec<String>) -> Vec<String> {
    values
        .into_iter()
        .filter_map(|item| normalize_optional_string(Some(item)))
        .collect()
}

fn create_ai_provider_id() -> String {
    format!("provider-{}", Uuid::new_v4().simple())
}

fn default_ai_provider_config() -> AiProviderConfig {
    let provider = ai::provider_name(None);

    AiProviderConfig {
        id: "default".to_string(),
        name: "默认提供商".to_string(),
        provider: provider.clone(),
        api_base: Some(ai::default_api_base().to_string()),
        api_key: Some(ai::default_api_key().to_string()),
        chat_model: Some(ai::default_chat_model().to_string()),
        image_model: Some(ai::default_image_model_for_provider(&provider).to_string()),
    }
}

fn normalize_ai_provider_configs(items: Vec<AiProviderConfig>) -> Vec<AiProviderConfig> {
    let mut seen_ids = HashSet::new();

    items
        .into_iter()
        .enumerate()
        .filter_map(|(index, item)| {
            let provider = normalize_optional_string(Some(item.provider))
                .unwrap_or_else(|| ai::provider_name(None));
            let api_base = normalize_optional_string(item.api_base);
            let api_key = normalize_optional_string(item.api_key);
            let chat_model = normalize_optional_string(item.chat_model);
            let image_model = normalize_optional_string(item.image_model);
            let has_content = !item.name.trim().is_empty()
                || !provider.trim().is_empty()
                || api_base.is_some()
                || api_key.is_some()
                || chat_model.is_some()
                || image_model.is_some();

            if !has_content {
                return None;
            }

            let mut id =
                normalize_optional_string(Some(item.id)).unwrap_or_else(create_ai_provider_id);
            if !seen_ids.insert(id.clone()) {
                id = create_ai_provider_id();
                seen_ids.insert(id.clone());
            }

            let name = normalize_optional_string(Some(item.name))
                .unwrap_or_else(|| format!("提供商 {}", index + 1));

            Some(AiProviderConfig {
                id,
                name,
                provider,
                api_base,
                api_key,
                chat_model,
                image_model,
            })
        })
        .collect()
}

fn legacy_ai_provider_config(model: &Model) -> Option<AiProviderConfig> {
    let provider = normalize_optional_string(model.ai_provider.clone());
    let api_base = normalize_optional_string(model.ai_api_base.clone());
    let api_key = normalize_optional_string(model.ai_api_key.clone());
    let chat_model = normalize_optional_string(model.ai_chat_model.clone());

    if provider.is_none() && api_base.is_none() && api_key.is_none() && chat_model.is_none() {
        return None;
    }

    let normalized_provider = provider.unwrap_or_else(|| ai::provider_name(None));

    Some(AiProviderConfig {
        id: "default".to_string(),
        name: "当前提供商".to_string(),
        provider: normalized_provider.clone(),
        api_base,
        api_key,
        chat_model,
        image_model: Some(ai::default_image_model_for_provider(&normalized_provider).to_string()),
    })
}

pub(crate) fn ai_provider_configs(model: &Model) -> Vec<AiProviderConfig> {
    let parsed = model
        .ai_providers
        .as_ref()
        .and_then(|value| serde_json::from_value::<Vec<AiProviderConfig>>(value.clone()).ok())
        .map(normalize_ai_provider_configs)
        .unwrap_or_default();

    if !parsed.is_empty() {
        parsed
    } else {
        legacy_ai_provider_config(model).into_iter().collect()
    }
}

pub(crate) fn active_ai_provider_id(model: &Model) -> Option<String> {
    let configs = ai_provider_configs(model);
    let requested = normalize_optional_string(model.ai_active_provider_id.clone());

    if let Some(active_id) = requested.filter(|id| configs.iter().any(|item| item.id == *id)) {
        Some(active_id)
    } else {
        configs.first().map(|item| item.id.clone())
    }
}

fn write_ai_provider_state(
    model: &mut Model,
    configs: Vec<AiProviderConfig>,
    requested_active_id: Option<String>,
) {
    let normalized = normalize_ai_provider_configs(configs);
    let active_id = requested_active_id
        .filter(|id| normalized.iter().any(|item| item.id == *id))
        .or_else(|| normalized.first().map(|item| item.id.clone()));

    model.ai_providers = (!normalized.is_empty()).then(|| serde_json::json!(normalized.clone()));
    model.ai_active_provider_id = active_id.clone();

    if let Some(active) = active_id.and_then(|id| normalized.into_iter().find(|item| item.id == id))
    {
        model.ai_provider = Some(active.provider);
        model.ai_api_base = active.api_base;
        model.ai_api_key = active.api_key;
        model.ai_chat_model = active.chat_model;
    } else {
        model.ai_provider = None;
        model.ai_api_base = None;
        model.ai_api_key = None;
        model.ai_chat_model = None;
    }
}

fn sync_ai_provider_fields(model: &mut Model) {
    write_ai_provider_state(
        model,
        ai_provider_configs(model),
        active_ai_provider_id(model),
    );
}

fn update_active_provider_from_legacy_fields(model: &mut Model) {
    let provider = model.ai_provider.clone();
    let api_base = model.ai_api_base.clone();
    let api_key = model.ai_api_key.clone();
    let chat_model = model.ai_chat_model.clone();
    let mut configs = ai_provider_configs(model);
    let active_id = active_ai_provider_id(model);

    if configs.is_empty() {
        let mut config = default_ai_provider_config();
        config.provider = provider.unwrap_or_else(|| ai::provider_name(None));
        config.api_base = api_base;
        config.api_key = api_key;
        config.chat_model = chat_model;
        config.image_model =
            Some(ai::default_image_model_for_provider(&config.provider).to_string());
        write_ai_provider_state(
            model,
            vec![config],
            Some(active_id.unwrap_or_else(|| "default".to_string())),
        );
        return;
    }

    let target_id = active_id
        .clone()
        .or_else(|| configs.first().map(|item| item.id.clone()));

    if let Some(target_id) = target_id {
        for config in &mut configs {
            if config.id == target_id {
                if let Some(next_provider) = provider.clone() {
                    config.provider = next_provider;
                }
                config.api_base = api_base.clone();
                config.api_key = api_key.clone();
                config.chat_model = chat_model.clone();
                if config.image_model.is_none() {
                    config.image_model =
                        Some(ai::default_image_model_for_provider(&config.provider).to_string());
                }
            }
        }
    }

    write_ai_provider_state(model, configs, active_id);
}

fn normalize_music_playlist(items: Vec<MusicTrackPayload>) -> Vec<MusicTrackPayload> {
    items
        .into_iter()
        .map(|item| MusicTrackPayload {
            title: item.title.trim().to_string(),
            artist: normalize_optional_string(item.artist),
            album: normalize_optional_string(item.album),
            url: item.url.trim().to_string(),
            cover_image_url: normalize_optional_string(item.cover_image_url),
            accent_color: normalize_optional_string(item.accent_color),
            description: normalize_optional_string(item.description),
        })
        .filter(|item| !item.title.is_empty() && !item.url.is_empty())
        .collect()
}

impl SiteSettingsPayload {
    pub(crate) fn apply(self, item: &mut Model) {
        if let Some(site_name) = self.site_name {
            item.site_name = normalize_optional_string(Some(site_name));
        }
        if let Some(site_short_name) = self.site_short_name {
            item.site_short_name = normalize_optional_string(Some(site_short_name));
        }
        if let Some(site_url) = self.site_url {
            item.site_url = normalize_optional_string(Some(site_url));
        }
        if let Some(site_title) = self.site_title {
            item.site_title = normalize_optional_string(Some(site_title));
        }
        if let Some(site_description) = self.site_description {
            item.site_description = normalize_optional_string(Some(site_description));
        }
        if let Some(hero_title) = self.hero_title {
            item.hero_title = normalize_optional_string(Some(hero_title));
        }
        if let Some(hero_subtitle) = self.hero_subtitle {
            item.hero_subtitle = normalize_optional_string(Some(hero_subtitle));
        }
        if let Some(owner_name) = self.owner_name {
            item.owner_name = normalize_optional_string(Some(owner_name));
        }
        if let Some(owner_title) = self.owner_title {
            item.owner_title = normalize_optional_string(Some(owner_title));
        }
        if let Some(owner_bio) = self.owner_bio {
            item.owner_bio = normalize_optional_string(Some(owner_bio));
        }
        if let Some(owner_avatar_url) = self.owner_avatar_url {
            item.owner_avatar_url = normalize_optional_string(Some(owner_avatar_url));
        }
        if let Some(social_github) = self.social_github {
            item.social_github = normalize_optional_string(Some(social_github));
        }
        if let Some(social_twitter) = self.social_twitter {
            item.social_twitter = normalize_optional_string(Some(social_twitter));
        }
        if let Some(social_email) = self.social_email {
            item.social_email = normalize_optional_string(Some(social_email));
        }
        if let Some(location) = self.location {
            item.location = normalize_optional_string(Some(location));
        }
        if let Some(tech_stack) = self.tech_stack {
            item.tech_stack = Some(serde_json::json!(tech_stack));
        }
        if let Some(music_playlist) = self.music_playlist {
            item.music_playlist = Some(serde_json::json!(normalize_music_playlist(music_playlist)));
        }
        if let Some(music_enabled) = self.music_enabled {
            item.music_enabled = Some(music_enabled);
        }
        if let Some(maintenance_mode_enabled) = self.maintenance_mode_enabled {
            item.maintenance_mode_enabled = Some(maintenance_mode_enabled);
        }
        if self.maintenance_access_code.is_some() {
            item.maintenance_access_code = normalize_optional_string(self.maintenance_access_code);
        }
        if let Some(ai_enabled) = self.ai_enabled {
            item.ai_enabled = Some(ai_enabled);
        }
        if let Some(paragraph_comments_enabled) = self.paragraph_comments_enabled {
            item.paragraph_comments_enabled = Some(paragraph_comments_enabled);
        }
        if let Some(comment_verification_mode) = self
            .comment_verification_mode
            .as_deref()
            .and_then(|value| crate::services::turnstile::normalize_verification_mode(Some(value)))
        {
            item.comment_verification_mode = Some(comment_verification_mode.as_str().to_string());
            item.comment_turnstile_enabled = Some(matches!(
                comment_verification_mode,
                crate::services::turnstile::VerificationMode::Turnstile
            ));
        } else if let Some(comment_turnstile_enabled) = self.comment_turnstile_enabled {
            item.comment_turnstile_enabled = Some(comment_turnstile_enabled);
            item.comment_verification_mode = Some(
                if comment_turnstile_enabled {
                    crate::services::turnstile::VERIFICATION_MODE_TURNSTILE
                } else {
                    crate::services::turnstile::VERIFICATION_MODE_CAPTCHA
                }
                .to_string(),
            );
        }
        if let Some(subscription_verification_mode) = self
            .subscription_verification_mode
            .as_deref()
            .and_then(|value| crate::services::turnstile::normalize_verification_mode(Some(value)))
        {
            item.subscription_verification_mode =
                Some(subscription_verification_mode.as_str().to_string());
            item.subscription_turnstile_enabled = Some(matches!(
                subscription_verification_mode,
                crate::services::turnstile::VerificationMode::Turnstile
            ));
        } else if let Some(subscription_turnstile_enabled) = self.subscription_turnstile_enabled {
            item.subscription_turnstile_enabled = Some(subscription_turnstile_enabled);
            item.subscription_verification_mode = Some(
                if subscription_turnstile_enabled {
                    crate::services::turnstile::VERIFICATION_MODE_TURNSTILE
                } else {
                    crate::services::turnstile::VERIFICATION_MODE_OFF
                }
                .to_string(),
            );
        }
        if let Some(web_push_enabled) = self.web_push_enabled {
            item.web_push_enabled = Some(web_push_enabled);
        }
        if let Some(turnstile_site_key) = self.turnstile_site_key {
            item.turnstile_site_key = normalize_optional_string(Some(turnstile_site_key));
        }
        if let Some(turnstile_secret_key) = self.turnstile_secret_key {
            item.turnstile_secret_key = normalize_optional_string(Some(turnstile_secret_key));
        }
        if let Some(web_push_vapid_public_key) = self.web_push_vapid_public_key {
            item.web_push_vapid_public_key =
                normalize_optional_string(Some(web_push_vapid_public_key));
        }
        if let Some(web_push_vapid_private_key) = self.web_push_vapid_private_key {
            item.web_push_vapid_private_key =
                normalize_optional_string(Some(web_push_vapid_private_key));
        }
        if let Some(web_push_vapid_subject) = self.web_push_vapid_subject {
            item.web_push_vapid_subject = normalize_optional_string(Some(web_push_vapid_subject));
        }
        let provider_list_supplied = self.ai_providers.is_some();
        let provided_ai_providers = self.ai_providers.map(normalize_ai_provider_configs);
        let requested_active_provider_id = self
            .ai_active_provider_id
            .and_then(|value| normalize_optional_string(Some(value)));
        let legacy_provider_fields_updated = self.ai_provider.is_some()
            || self.ai_api_base.is_some()
            || self.ai_api_key.is_some()
            || self.ai_chat_model.is_some();
        if let Some(ai_provider) = self.ai_provider {
            item.ai_provider = normalize_optional_string(Some(ai_provider));
        }
        if let Some(ai_api_base) = self.ai_api_base {
            item.ai_api_base = normalize_optional_string(Some(ai_api_base));
        }
        if let Some(ai_api_key) = self.ai_api_key {
            item.ai_api_key = normalize_optional_string(Some(ai_api_key));
        }
        if let Some(ai_chat_model) = self.ai_chat_model {
            item.ai_chat_model = normalize_optional_string(Some(ai_chat_model));
        }
        if let Some(ai_image_provider) = self.ai_image_provider {
            item.ai_image_provider = normalize_optional_string(Some(ai_image_provider));
        }
        if let Some(ai_image_api_base) = self.ai_image_api_base {
            item.ai_image_api_base = normalize_optional_string(Some(ai_image_api_base));
        }
        if let Some(ai_image_api_key) = self.ai_image_api_key {
            item.ai_image_api_key = normalize_optional_string(Some(ai_image_api_key));
        }
        if let Some(ai_image_model) = self.ai_image_model {
            item.ai_image_model = normalize_optional_string(Some(ai_image_model));
        }
        if let Some(ai_embedding_model) = self.ai_embedding_model {
            item.ai_embedding_model = normalize_optional_string(Some(ai_embedding_model));
        }
        if let Some(ai_system_prompt) = self.ai_system_prompt {
            item.ai_system_prompt = normalize_optional_string(Some(ai_system_prompt));
        }
        if self.ai_top_k.is_some() {
            item.ai_top_k = normalize_optional_int(self.ai_top_k, 1, 12);
        }
        if self.ai_chunk_size.is_some() {
            item.ai_chunk_size = normalize_optional_int(self.ai_chunk_size, 400, 4000);
        }
        if let Some(media_r2_account_id) = self.media_r2_account_id {
            item.media_r2_account_id = normalize_optional_string(Some(media_r2_account_id));
        }
        if let Some(media_storage_provider) = self.media_storage_provider {
            item.media_storage_provider = normalize_optional_string(Some(media_storage_provider));
        }
        if let Some(media_r2_bucket) = self.media_r2_bucket {
            item.media_r2_bucket = normalize_optional_string(Some(media_r2_bucket));
        }
        if let Some(media_r2_public_base_url) = self.media_r2_public_base_url {
            item.media_r2_public_base_url =
                normalize_optional_string(Some(media_r2_public_base_url));
        }
        if let Some(media_r2_access_key_id) = self.media_r2_access_key_id {
            item.media_r2_access_key_id = normalize_optional_string(Some(media_r2_access_key_id));
        }
        if let Some(media_r2_secret_access_key) = self.media_r2_secret_access_key {
            item.media_r2_secret_access_key =
                normalize_optional_string(Some(media_r2_secret_access_key));
        }
        if let Some(seo_favicon_url) = self.seo_favicon_url {
            item.seo_favicon_url = normalize_optional_string(Some(seo_favicon_url));
        }
        if let Some(seo_default_og_image) = self.seo_default_og_image {
            item.seo_default_og_image = normalize_optional_string(Some(seo_default_og_image));
        }
        if let Some(seo_default_twitter_handle) = self.seo_default_twitter_handle {
            item.seo_default_twitter_handle =
                normalize_optional_string(Some(seo_default_twitter_handle));
        }
        if let Some(seo_wechat_share_qr_enabled) = self.seo_wechat_share_qr_enabled {
            item.seo_wechat_share_qr_enabled = Some(seo_wechat_share_qr_enabled);
        }
        if let Some(notification_webhook_url) = self.notification_webhook_url {
            item.notification_webhook_url =
                normalize_optional_string(Some(notification_webhook_url));
        }
        if self.notification_channel_type.is_some() {
            item.notification_channel_type =
                normalize_notification_channel_type(self.notification_channel_type);
        }
        if let Some(notification_comment_enabled) = self.notification_comment_enabled {
            item.notification_comment_enabled = Some(notification_comment_enabled);
        }
        if let Some(notification_friend_link_enabled) = self.notification_friend_link_enabled {
            item.notification_friend_link_enabled = Some(notification_friend_link_enabled);
        }
        if let Some(subscription_popup_enabled) = self.subscription_popup_enabled {
            item.subscription_popup_enabled = Some(subscription_popup_enabled);
        }
        if let Some(subscription_popup_title) = self.subscription_popup_title {
            item.subscription_popup_title =
                normalize_optional_string(Some(subscription_popup_title));
        }
        if let Some(subscription_popup_description) = self.subscription_popup_description {
            item.subscription_popup_description =
                normalize_optional_string(Some(subscription_popup_description));
        }
        if self.subscription_popup_delay_seconds.is_some() {
            item.subscription_popup_delay_seconds =
                normalize_optional_int(self.subscription_popup_delay_seconds, 3, 120);
        }
        if let Some(search_synonyms) = self.search_synonyms {
            let normalized = normalize_string_list(search_synonyms);
            item.search_synonyms = (!normalized.is_empty()).then(|| serde_json::json!(normalized));
        }

        if provider_list_supplied {
            write_ai_provider_state(
                item,
                provided_ai_providers.unwrap_or_default(),
                requested_active_provider_id.or_else(|| item.ai_active_provider_id.clone()),
            );
        } else if legacy_provider_fields_updated {
            update_active_provider_from_legacy_fields(item);
        } else {
            sync_ai_provider_fields(item);
        }
    }
}

fn default_payload() -> SiteSettingsPayload {
    SiteSettingsPayload {
        site_name: Some("InitCool".to_string()),
        site_short_name: Some("Termi".to_string()),
        site_url: Some("https://init.cool".to_string()),
        site_title: Some("InitCool · 技术笔记与内容档案".to_string()),
        site_description: Some("围绕开发实践、产品观察与长期积累整理的中文内容站。".to_string()),
        hero_title: Some("欢迎来到 InitCool".to_string()),
        hero_subtitle: Some("记录开发实践、产品观察与长期积累，分享清晰、耐读、可回看的内容。".to_string()),
        owner_name: Some("InitCool".to_string()),
        owner_title: Some("Rust / Go / Python Developer · Builder @ init.cool".to_string()),
        owner_bio: Some(
            "InitCool，GitHub 用户名 limitcool。坚持不要重复造轮子，当前在维护 starter，平时主要写 Rust、Go、Python 相关项目，也在持续学习 AI 与 Web3。"
                .to_string(),
        ),
        owner_avatar_url: Some("https://github.com/limitcool.png".to_string()),
        social_github: Some("https://github.com/limitcool".to_string()),
        social_twitter: None,
        social_email: Some("mailto:initcoool@gmail.com".to_string()),
        location: Some("Hong Kong".to_string()),
        tech_stack: Some(vec![
            "Rust".to_string(),
            "Go".to_string(),
            "Python".to_string(),
            "Svelte".to_string(),
            "Astro".to_string(),
            "Loco.rs".to_string(),
        ]),
        music_playlist: Some(vec![
            MusicTrackPayload {
                title: "山中来信".to_string(),
                artist: Some("InitCool Radio".to_string()),
                album: Some("站点默认歌单".to_string()),
                url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3".to_string(),
                cover_image_url: Some(
                    "https://images.unsplash.com/photo-1510915228340-29c85a43dcfe?auto=format&fit=crop&w=600&q=80"
                        .to_string(),
                ),
                accent_color: Some("#2f6b5f".to_string()),
                description: Some("适合文章阅读时循环播放的轻氛围曲。".to_string()),
            },
            MusicTrackPayload {
                title: "风吹松声".to_string(),
                artist: Some("InitCool Radio".to_string()),
                album: Some("站点默认歌单".to_string()),
                url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3".to_string(),
                cover_image_url: Some(
                    "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=600&q=80"
                        .to_string(),
                ),
                accent_color: Some("#8a5b35".to_string()),
                description: Some("偏木质感的器乐氛围，适合深夜浏览。".to_string()),
            },
            MusicTrackPayload {
                title: "夜航小记".to_string(),
                artist: Some("InitCool Radio".to_string()),
                album: Some("站点默认歌单".to_string()),
                url: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3".to_string(),
                cover_image_url: Some(
                    "https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?auto=format&fit=crop&w=600&q=80"
                        .to_string(),
                ),
                accent_color: Some("#375a7f".to_string()),
                description: Some("节奏更明显一点，适合切换阅读状态。".to_string()),
            },
        ]),
        music_enabled: Some(true),
        maintenance_mode_enabled: Some(false),
        maintenance_access_code: None,
        ai_enabled: Some(false),
        paragraph_comments_enabled: Some(true),
        comment_verification_mode: Some(
            crate::services::turnstile::VERIFICATION_MODE_CAPTCHA.to_string(),
        ),
        comment_turnstile_enabled: Some(false),
        subscription_verification_mode: Some(
            crate::services::turnstile::VERIFICATION_MODE_OFF.to_string(),
        ),
        subscription_turnstile_enabled: Some(false),
        web_push_enabled: Some(false),
        turnstile_site_key: Some(DEFAULT_TURNSTILE_SITE_KEY.to_string()),
        turnstile_secret_key: Some(DEFAULT_TURNSTILE_SECRET_KEY.to_string()),
        web_push_vapid_public_key: None,
        web_push_vapid_private_key: None,
        web_push_vapid_subject: None,
        ai_provider: Some(ai::provider_name(None)),
        ai_api_base: Some(ai::default_api_base().to_string()),
        ai_api_key: Some(ai::default_api_key().to_string()),
        ai_chat_model: Some(ai::default_chat_model().to_string()),
        ai_image_provider: None,
        ai_image_api_base: None,
        ai_image_api_key: None,
        ai_image_model: None,
        ai_providers: Some(vec![default_ai_provider_config()]),
        ai_active_provider_id: Some("default".to_string()),
        ai_embedding_model: Some(ai::local_embedding_label().to_string()),
        ai_system_prompt: Some(
            "你是这个博客的站内 AI 助手。请优先基于提供的上下文回答，答案要准确、简洁、实用；如果上下文不足，请明确说明。"
                .to_string(),
        ),
        ai_top_k: Some(4),
        ai_chunk_size: Some(1200),
        media_storage_provider: None,
        media_r2_account_id: None,
        media_r2_bucket: None,
        media_r2_public_base_url: None,
        media_r2_access_key_id: None,
        media_r2_secret_access_key: None,
        seo_favicon_url: None,
        seo_default_og_image: None,
        seo_default_twitter_handle: None,
        seo_wechat_share_qr_enabled: Some(false),
        notification_webhook_url: None,
        notification_channel_type: Some("webhook".to_string()),
        notification_comment_enabled: Some(false),
        notification_friend_link_enabled: Some(false),
        subscription_popup_enabled: Some(default_subscription_popup_enabled()),
        subscription_popup_title: Some(default_subscription_popup_title()),
        subscription_popup_description: Some(default_subscription_popup_description()),
        subscription_popup_delay_seconds: Some(default_subscription_popup_delay_seconds()),
        search_synonyms: Some(Vec::new()),
    }
}

pub(crate) async fn load_current(ctx: &AppContext) -> Result<Model> {
    if let Some(settings) = Entity::find()
        .order_by_asc(site_settings::Column::Id)
        .one(&ctx.db)
        .await?
    {
        return Ok(settings);
    }

    let inserted = ActiveModel {
        id: Set(1),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await?;
    let mut model = inserted;
    default_payload().apply(&mut model);
    Ok(model
        .into_active_model()
        .reset_all()
        .update(&ctx.db)
        .await?)
}

fn public_response(model: Model) -> PublicSiteSettingsResponse {
    let turnstile_site_key = crate::services::turnstile::site_key(&model);
    let web_push_vapid_public_key = crate::services::web_push::public_key(&model);
    let comment_verification_mode = crate::services::turnstile::effective_mode(
        &model,
        crate::services::turnstile::TurnstileScope::Comment,
    );
    let subscription_verification_mode = crate::services::turnstile::effective_mode(
        &model,
        crate::services::turnstile::TurnstileScope::Subscription,
    );
    let web_push_enabled = crate::services::web_push::is_enabled(&model);

    PublicSiteSettingsResponse {
        id: model.id,
        site_name: model.site_name,
        site_short_name: model.site_short_name,
        site_url: model.site_url,
        site_title: model.site_title,
        site_description: model.site_description,
        hero_title: model.hero_title,
        hero_subtitle: model.hero_subtitle,
        owner_name: model.owner_name,
        owner_title: model.owner_title,
        owner_bio: model.owner_bio,
        owner_avatar_url: model.owner_avatar_url,
        social_github: model.social_github,
        social_twitter: model.social_twitter,
        social_email: model.social_email,
        location: model.location,
        tech_stack: model.tech_stack,
        music_playlist: model.music_playlist,
        music_enabled: model.music_enabled.unwrap_or(true),
        ai_enabled: model.ai_enabled.unwrap_or(false),
        paragraph_comments_enabled: model.paragraph_comments_enabled.unwrap_or(true),
        comment_verification_mode: comment_verification_mode.as_str().to_string(),
        comment_turnstile_enabled: matches!(
            comment_verification_mode,
            crate::services::turnstile::VerificationMode::Turnstile
        ),
        subscription_verification_mode: subscription_verification_mode.as_str().to_string(),
        subscription_turnstile_enabled: matches!(
            subscription_verification_mode,
            crate::services::turnstile::VerificationMode::Turnstile
        ),
        web_push_enabled,
        turnstile_site_key,
        web_push_vapid_public_key,
        subscription_popup_enabled: model
            .subscription_popup_enabled
            .unwrap_or_else(default_subscription_popup_enabled),
        subscription_popup_title: model
            .subscription_popup_title
            .unwrap_or_else(default_subscription_popup_title),
        subscription_popup_description: model
            .subscription_popup_description
            .unwrap_or_else(default_subscription_popup_description),
        subscription_popup_delay_seconds: model
            .subscription_popup_delay_seconds
            .unwrap_or_else(default_subscription_popup_delay_seconds),
        seo_favicon_url: model.seo_favicon_url,
        seo_default_og_image: model.seo_default_og_image,
        seo_default_twitter_handle: model.seo_default_twitter_handle,
        seo_wechat_share_qr_enabled: model.seo_wechat_share_qr_enabled.unwrap_or(false),
    }
}

#[debug_handler]
pub async fn home(State(ctx): State<AppContext>) -> Result<Response> {
    let site_settings = public_response(load_current(&ctx).await?);
    let posts = posts::Entity::find()
        .order_by_desc(posts::Column::CreatedAt)
        .all(&ctx.db)
        .await?
        .into_iter()
        .filter(|post| content::is_post_listed_publicly(post, chrono::Utc::now().fixed_offset()))
        .collect::<Vec<_>>();
    let tags = tags::Entity::find().all(&ctx.db).await?;
    let friend_links = friend_links::Entity::find()
        .filter(friend_links::Column::Status.eq("approved"))
        .order_by_desc(friend_links::Column::CreatedAt)
        .all(&ctx.db)
        .await?;
    let category_items = categories::Entity::find()
        .order_by_asc(categories::Column::Slug)
        .all(&ctx.db)
        .await?;

    let categories = category_items
        .into_iter()
        .map(|category| {
            let name = category
                .name
                .clone()
                .unwrap_or_else(|| category.slug.clone());
            let count = posts
                .iter()
                .filter(|post| post.category.as_deref().map(str::trim) == Some(name.as_str()))
                .count();

            HomeCategorySummary {
                id: category.id,
                name,
                slug: category.slug,
                count,
            }
        })
        .collect::<Vec<_>>();
    let content_highlights =
        crate::services::analytics::build_public_content_highlights(&ctx, &posts).await?;
    let content_ranges =
        crate::services::analytics::build_public_content_windows(&ctx, &posts).await?;

    format::json(HomePageResponse {
        site_settings,
        posts,
        tags,
        friend_links,
        categories,
        content_overview: content_highlights.overview,
        popular_posts: content_highlights.popular_posts,
        content_ranges,
    })
}

#[debug_handler]
pub async fn show(State(ctx): State<AppContext>) -> Result<Response> {
    format::json(public_response(load_current(&ctx).await?))
}

#[debug_handler]
pub async fn maintenance_status(
    State(ctx): State<AppContext>,
    Json(params): Json<MaintenanceAccessTokenPayload>,
) -> Result<Response> {
    let current = load_current(&ctx).await?;
    let enabled = maintenance_mode_enabled(&current);
    let access_granted = if enabled {
        validate_maintenance_access_token(&current, params.access_token.as_deref())
    } else {
        true
    };

    format::json(MaintenanceAccessStatusResponse {
        maintenance_mode_enabled: enabled,
        access_granted,
    })
}

#[debug_handler]
pub async fn maintenance_verify(
    State(ctx): State<AppContext>,
    Json(params): Json<MaintenanceVerifyPayload>,
) -> Result<Response> {
    let current = load_current(&ctx).await?;
    let enabled = maintenance_mode_enabled(&current);

    if !enabled {
        return format::json(MaintenanceVerifyResponse {
            maintenance_mode_enabled: false,
            access_granted: true,
            access_token: None,
        });
    }

    let access_token = verify_maintenance_access_code(&current, params.code.as_deref());

    format::json(MaintenanceVerifyResponse {
        maintenance_mode_enabled: true,
        access_granted: access_token.is_some(),
        access_token,
    })
}

#[debug_handler]
pub async fn update(
    headers: HeaderMap,
    State(ctx): State<AppContext>,
    Json(params): Json<SiteSettingsPayload>,
) -> Result<Response> {
    check_auth(&headers)?;

    let current = load_current(&ctx).await?;
    let mut item = current;
    params.apply(&mut item);
    let item = item.into_active_model().reset_all();
    let updated = item.update(&ctx.db).await?;
    format::json(public_response(updated))
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("api/site_settings/")
        .add("home", get(home))
        .add("maintenance/status", post(maintenance_status))
        .add("maintenance/verify", post(maintenance_verify))
        .add("/", get(show))
        .add("/", put(update))
        .add("/", patch(update))
}
