use std::path::Path;

use chrono::Utc;
use loco_rs::prelude::*;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, QueryFilter,
    QueryOrder, Set,
};
use serde::{Deserialize, Serialize};

use crate::{
    controllers::site_settings,
    models::_entities::{
        categories, comments, friend_links, media_assets, posts, reviews,
        site_settings as site_settings_entity, tags,
    },
    services::{content, media_assets as media_assets_service, storage},
};

const BACKUP_VERSION: &str = "2026-04-01";
const WARNING_STORAGE_BINARIES: &str = "当前备份只包含内容、配置与对象清单，不包含对象存储二进制文件本身。恢复后如需图片等资源，仍需保留原对象存储桶或手动回传文件。";

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BackupTaxonomyRecord {
    pub name: String,
    pub slug: String,
    pub description: Option<String>,
    pub cover_image: Option<String>,
    pub accent_color: Option<String>,
    pub seo_title: Option<String>,
    pub seo_description: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BackupReviewRecord {
    pub title: Option<String>,
    pub review_type: Option<String>,
    pub rating: Option<i32>,
    pub review_date: Option<String>,
    pub status: Option<String>,
    pub description: Option<String>,
    pub tags: Option<String>,
    pub cover: Option<String>,
    pub link_url: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BackupFriendLinkRecord {
    pub site_name: Option<String>,
    pub site_url: String,
    pub avatar_url: Option<String>,
    pub description: Option<String>,
    pub category: Option<String>,
    pub status: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BackupMediaAssetRecord {
    pub object_key: String,
    pub title: Option<String>,
    pub alt_text: Option<String>,
    pub caption: Option<String>,
    pub tags: Vec<String>,
    pub notes: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BackupStorageObjectRecord {
    pub key: String,
    pub url: String,
    pub size_bytes: i64,
    pub last_modified: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BackupPostDocument {
    pub slug: String,
    pub file_name: String,
    pub markdown: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SiteBackupDocument {
    pub version: String,
    pub exported_at: String,
    pub includes_storage_binaries: bool,
    pub warning: String,
    pub site_settings: site_settings_entity::Model,
    pub categories: Vec<BackupTaxonomyRecord>,
    pub tags: Vec<BackupTaxonomyRecord>,
    pub reviews: Vec<BackupReviewRecord>,
    pub friend_links: Vec<BackupFriendLinkRecord>,
    pub media_assets: Vec<BackupMediaAssetRecord>,
    pub storage_manifest: Option<Vec<BackupStorageObjectRecord>>,
    pub posts: Vec<BackupPostDocument>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SiteBackupImportSummary {
    pub imported: bool,
    pub mode: String,
    pub site_settings_restored: bool,
    pub posts_written: usize,
    pub categories_upserted: usize,
    pub tags_upserted: usize,
    pub reviews_upserted: usize,
    pub friend_links_upserted: usize,
    pub media_assets_upserted: usize,
    pub storage_manifest_items: usize,
    pub includes_storage_binaries: bool,
    pub warning: 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 slugify(value: &str) -> String {
    let mut slug = String::new();
    let mut last_was_dash = false;

    for ch in value.trim().chars() {
        if ch.is_ascii_alphanumeric() {
            slug.push(ch.to_ascii_lowercase());
            last_was_dash = false;
        } else if (ch.is_whitespace() || ch == '-' || ch == '_') && !last_was_dash {
            slug.push('-');
            last_was_dash = true;
        }
    }

    slug.trim_matches('-').to_string()
}

fn normalize_backup_mode(value: Option<&str>) -> String {
    match value
        .map(str::trim)
        .unwrap_or("merge")
        .to_ascii_lowercase()
        .as_str()
    {
        "replace" => "replace".to_string(),
        _ => "merge".to_string(),
    }
}

fn normalize_markdown(value: &str) -> String {
    value.replace("\r\n", "\n")
}

fn normalized_backup_post(document: &BackupPostDocument) -> Result<(String, String)> {
    let candidate_slug = trim_to_option(Some(document.slug.clone())).unwrap_or_default();
    let file_name = trim_to_option(Some(document.file_name.clone())).unwrap_or_else(|| {
        format!(
            "{}.md",
            if candidate_slug.is_empty() {
                "post"
            } else {
                &candidate_slug
            }
        )
    });
    let file_stem = Path::new(&file_name)
        .file_stem()
        .and_then(|value| value.to_str())
        .unwrap_or("post");
    let markdown = normalize_markdown(&document.markdown);
    let parsed = content::parse_markdown_source(file_stem, &markdown, &file_name)?;
    let slug = if parsed.slug.trim().is_empty() {
        candidate_slug
    } else {
        parsed.slug
    };

    if slug.trim().is_empty() {
        return Err(Error::BadRequest("备份中的文章 slug 不能为空".to_string()));
    }

    Ok((slug, markdown))
}

async fn export_storage_manifest(
    ctx: &AppContext,
) -> Result<Option<Vec<BackupStorageObjectRecord>>> {
    if storage::optional_r2_settings(ctx).await?.is_none() {
        return Ok(None);
    }

    Ok(Some(
        storage::list_objects(ctx, None, 1000)
            .await?
            .into_iter()
            .map(|item| BackupStorageObjectRecord {
                key: item.key,
                url: item.url,
                size_bytes: item.size_bytes,
                last_modified: item.last_modified,
            })
            .collect(),
    ))
}

fn export_category_record(item: categories::Model) -> BackupTaxonomyRecord {
    BackupTaxonomyRecord {
        name: item.name.unwrap_or_else(|| item.slug.clone()),
        slug: item.slug,
        description: item.description,
        cover_image: item.cover_image,
        accent_color: item.accent_color,
        seo_title: item.seo_title,
        seo_description: item.seo_description,
    }
}

fn export_tag_record(item: tags::Model) -> BackupTaxonomyRecord {
    BackupTaxonomyRecord {
        name: item.name.unwrap_or_else(|| item.slug.clone()),
        slug: item.slug,
        description: item.description,
        cover_image: item.cover_image,
        accent_color: item.accent_color,
        seo_title: item.seo_title,
        seo_description: item.seo_description,
    }
}

fn export_review_record(item: reviews::Model) -> BackupReviewRecord {
    BackupReviewRecord {
        title: item.title,
        review_type: item.review_type,
        rating: item.rating,
        review_date: item.review_date,
        status: item.status,
        description: item.description,
        tags: item.tags,
        cover: item.cover,
        link_url: item.link_url,
    }
}

fn export_friend_link_record(item: friend_links::Model) -> BackupFriendLinkRecord {
    BackupFriendLinkRecord {
        site_name: item.site_name,
        site_url: item.site_url,
        avatar_url: item.avatar_url,
        description: item.description,
        category: item.category,
        status: item.status,
    }
}

fn export_media_asset_record(item: media_assets::Model) -> BackupMediaAssetRecord {
    let tags = media_assets_service::tag_list(&item);
    BackupMediaAssetRecord {
        object_key: item.object_key,
        title: item.title,
        alt_text: item.alt_text,
        caption: item.caption,
        tags,
        notes: item.notes,
    }
}

pub async fn export_site_backup(ctx: &AppContext) -> Result<SiteBackupDocument> {
    let site_settings_row = site_settings::load_current(ctx).await?;
    let categories = categories::Entity::find()
        .order_by_asc(categories::Column::Slug)
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(export_category_record)
        .collect::<Vec<_>>();
    let tags = tags::Entity::find()
        .order_by_asc(tags::Column::Slug)
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(export_tag_record)
        .collect::<Vec<_>>();
    let reviews = reviews::Entity::find()
        .order_by_desc(reviews::Column::UpdatedAt)
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(export_review_record)
        .collect::<Vec<_>>();
    let friend_links = friend_links::Entity::find()
        .order_by_asc(friend_links::Column::SiteUrl)
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(export_friend_link_record)
        .collect::<Vec<_>>();
    let media_assets = media_assets::Entity::find()
        .order_by_asc(media_assets::Column::ObjectKey)
        .all(&ctx.db)
        .await?
        .into_iter()
        .map(export_media_asset_record)
        .collect::<Vec<_>>();
    let posts = content::load_markdown_posts_from_store(ctx)
        .await?
        .into_iter()
        .map(|post| {
            Ok(BackupPostDocument {
                slug: post.slug.clone(),
                file_name: format!("{}.md", post.slug),
                markdown: content::build_markdown_document(&post),
            })
        })
        .collect::<Result<Vec<_>>>()?;
    let storage_manifest = match export_storage_manifest(ctx).await {
        Ok(items) => items,
        Err(error) => {
            tracing::warn!(
                ?error,
                "failed to export storage manifest, continuing without it"
            );
            None
        }
    };

    Ok(SiteBackupDocument {
        version: BACKUP_VERSION.to_string(),
        exported_at: Utc::now().to_rfc3339(),
        includes_storage_binaries: false,
        warning: WARNING_STORAGE_BINARIES.to_string(),
        site_settings: site_settings_row,
        categories,
        tags,
        reviews,
        friend_links,
        media_assets,
        storage_manifest,
        posts,
    })
}

async fn restore_site_settings(
    ctx: &AppContext,
    value: &site_settings_entity::Model,
) -> Result<()> {
    let current = site_settings::load_current(ctx).await?;
    let mut active = value.clone().into_active_model();
    active.id = Set(current.id);
    active.created_at = Set(current.created_at);
    active.updated_at = Set(Utc::now().into());
    active.reset_all().update(&ctx.db).await?;
    Ok(())
}

async fn upsert_category(ctx: &AppContext, item: &BackupTaxonomyRecord) -> Result<()> {
    let name = trim_to_option(Some(item.name.clone())).unwrap_or_else(|| item.slug.clone());
    let slug = trim_to_option(Some(item.slug.clone())).unwrap_or_else(|| slugify(&name));
    if slug.is_empty() {
        return Err(Error::BadRequest("分类 slug 不能为空".to_string()));
    }

    let existing = categories::Entity::find()
        .filter(
            Condition::any()
                .add(categories::Column::Slug.eq(&slug))
                .add(categories::Column::Name.eq(name.clone())),
        )
        .one(&ctx.db)
        .await?;

    let has_existing = existing.is_some();
    let mut active = existing
        .map(|model| model.into_active_model())
        .unwrap_or_default();
    active.name = Set(Some(name));
    active.slug = Set(slug);
    active.description = Set(trim_to_option(item.description.clone()));
    active.cover_image = Set(trim_to_option(item.cover_image.clone()));
    active.accent_color = Set(trim_to_option(item.accent_color.clone()));
    active.seo_title = Set(trim_to_option(item.seo_title.clone()));
    active.seo_description = Set(trim_to_option(item.seo_description.clone()));

    if has_existing {
        active.update(&ctx.db).await?;
    } else {
        active.insert(&ctx.db).await?;
    }

    Ok(())
}

async fn upsert_tag(ctx: &AppContext, item: &BackupTaxonomyRecord) -> Result<()> {
    let name = trim_to_option(Some(item.name.clone())).unwrap_or_else(|| item.slug.clone());
    let slug = trim_to_option(Some(item.slug.clone())).unwrap_or_else(|| slugify(&name));
    if slug.is_empty() {
        return Err(Error::BadRequest("标签 slug 不能为空".to_string()));
    }

    let existing = tags::Entity::find()
        .filter(
            Condition::any()
                .add(tags::Column::Slug.eq(&slug))
                .add(tags::Column::Name.eq(name.clone())),
        )
        .one(&ctx.db)
        .await?;

    let has_existing = existing.is_some();
    let mut active = existing
        .map(|model| model.into_active_model())
        .unwrap_or_default();
    active.name = Set(Some(name));
    active.slug = Set(slug);
    active.description = Set(trim_to_option(item.description.clone()));
    active.cover_image = Set(trim_to_option(item.cover_image.clone()));
    active.accent_color = Set(trim_to_option(item.accent_color.clone()));
    active.seo_title = Set(trim_to_option(item.seo_title.clone()));
    active.seo_description = Set(trim_to_option(item.seo_description.clone()));

    if has_existing {
        active.update(&ctx.db).await?;
    } else {
        active.insert(&ctx.db).await?;
    }

    Ok(())
}

async fn upsert_friend_link(ctx: &AppContext, item: &BackupFriendLinkRecord) -> Result<()> {
    let site_url = trim_to_option(Some(item.site_url.clone()))
        .ok_or_else(|| Error::BadRequest("友链 site_url 不能为空".to_string()))?;

    let existing = friend_links::Entity::find()
        .filter(friend_links::Column::SiteUrl.eq(&site_url))
        .one(&ctx.db)
        .await?;
    let has_existing = existing.is_some();
    let mut active = existing
        .map(|model| model.into_active_model())
        .unwrap_or_default();

    active.site_name = Set(trim_to_option(item.site_name.clone()));
    active.site_url = Set(site_url);
    active.avatar_url = Set(trim_to_option(item.avatar_url.clone()));
    active.description = Set(trim_to_option(item.description.clone()));
    active.category = Set(trim_to_option(item.category.clone()));
    active.status = Set(trim_to_option(item.status.clone()));

    if has_existing {
        active.update(&ctx.db).await?;
    } else {
        active.insert(&ctx.db).await?;
    }

    Ok(())
}

async fn upsert_review(ctx: &AppContext, item: &BackupReviewRecord) -> Result<()> {
    let title = trim_to_option(item.title.clone());
    let review_type = trim_to_option(item.review_type.clone());
    let review_date = trim_to_option(item.review_date.clone());

    let mut query = reviews::Entity::find();
    if let Some(value) = title.clone() {
        query = query.filter(reviews::Column::Title.eq(value));
    }
    if let Some(value) = review_type.clone() {
        query = query.filter(reviews::Column::ReviewType.eq(value));
    }
    if let Some(value) = review_date.clone() {
        query = query.filter(reviews::Column::ReviewDate.eq(value));
    }

    let existing = if title.is_some() || review_type.is_some() || review_date.is_some() {
        query.order_by_asc(reviews::Column::Id).one(&ctx.db).await?
    } else {
        None
    };

    let has_existing = existing.is_some();
    let mut active = existing
        .map(|model| model.into_active_model())
        .unwrap_or_default();
    active.title = Set(title);
    active.review_type = Set(review_type);
    active.rating = Set(item.rating);
    active.review_date = Set(review_date);
    active.status = Set(trim_to_option(item.status.clone()));
    active.description = Set(trim_to_option(item.description.clone()));
    active.tags = Set(trim_to_option(item.tags.clone()));
    active.cover = Set(trim_to_option(item.cover.clone()));
    active.link_url = Set(trim_to_option(item.link_url.clone()));

    if has_existing {
        active.update(&ctx.db).await?;
    } else {
        active.insert(&ctx.db).await?;
    }

    Ok(())
}

async fn upsert_media_asset(ctx: &AppContext, item: &BackupMediaAssetRecord) -> Result<()> {
    media_assets_service::upsert_by_key(
        ctx,
        &item.object_key,
        media_assets_service::MediaAssetMetadataInput {
            title: item.title.clone(),
            alt_text: item.alt_text.clone(),
            caption: item.caption.clone(),
            tags: Some(item.tags.clone()),
            notes: item.notes.clone(),
        },
    )
    .await?;
    Ok(())
}

async fn write_backup_posts(
    ctx: &AppContext,
    documents: &[BackupPostDocument],
    replace_existing: bool,
) -> Result<usize> {
    if replace_existing {
        let existing_posts = posts::Entity::find().all(&ctx.db).await?;
        for post in &existing_posts {
            let related_comments = comments::Entity::find()
                .filter(comments::Column::PostSlug.eq(&post.slug))
                .all(&ctx.db)
                .await?;

            for comment in related_comments {
                let _ = comment.delete(&ctx.db).await;
            }
        }
        posts::Entity::delete_many().exec(&ctx.db).await?;
    }

    if documents.is_empty() {
        return Ok(0);
    }

    let mut written = std::collections::HashSet::new();
    for document in documents {
        let (slug, markdown) = normalized_backup_post(document)?;
        content::upsert_markdown_document(ctx, Some(&slug), &markdown).await?;
        written.insert(slug);
    }

    Ok(written.len())
}

pub async fn import_site_backup(
    ctx: &AppContext,
    backup: SiteBackupDocument,
    mode: Option<&str>,
) -> Result<SiteBackupImportSummary> {
    let mode = normalize_backup_mode(mode);
    let replace_existing = mode == "replace";

    if replace_existing {
        friend_links::Entity::delete_many().exec(&ctx.db).await?;
        reviews::Entity::delete_many().exec(&ctx.db).await?;
        media_assets::Entity::delete_many().exec(&ctx.db).await?;
        categories::Entity::delete_many().exec(&ctx.db).await?;
        tags::Entity::delete_many().exec(&ctx.db).await?;
    }

    restore_site_settings(ctx, &backup.site_settings).await?;
    let posts_written = write_backup_posts(ctx, &backup.posts, replace_existing).await?;

    let mut categories_upserted = 0_usize;
    for item in &backup.categories {
        upsert_category(ctx, item).await?;
        categories_upserted += 1;
    }

    let mut tags_upserted = 0_usize;
    for item in &backup.tags {
        upsert_tag(ctx, item).await?;
        tags_upserted += 1;
    }

    let mut reviews_upserted = 0_usize;
    for item in &backup.reviews {
        upsert_review(ctx, item).await?;
        reviews_upserted += 1;
    }

    let mut friend_links_upserted = 0_usize;
    for item in &backup.friend_links {
        upsert_friend_link(ctx, item).await?;
        friend_links_upserted += 1;
    }

    let mut media_assets_upserted = 0_usize;
    for item in &backup.media_assets {
        upsert_media_asset(ctx, item).await?;
        media_assets_upserted += 1;
    }

    Ok(SiteBackupImportSummary {
        imported: true,
        mode,
        site_settings_restored: true,
        posts_written,
        categories_upserted,
        tags_upserted,
        reviews_upserted,
        friend_links_upserted,
        media_assets_upserted,
        storage_manifest_items: backup.storage_manifest.as_ref().map(Vec::len).unwrap_or(0),
        includes_storage_binaries: backup.includes_storage_binaries,
        warning: WARNING_STORAGE_BINARIES.to_string(),
    })
}
