use loco_rs::prelude::*;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, EntityTrait, Order, QueryFilter, QueryOrder, QuerySelect, Set,
};

use crate::{
    controllers::admin::AdminIdentity,
    models::_entities::{post_revisions, posts},
    services::content,
};

#[derive(Clone, Copy, Debug)]
pub enum RestoreMode {
    Full,
    Markdown,
    Metadata,
}

impl RestoreMode {
    pub fn parse(value: &str) -> Self {
        match value.trim().to_ascii_lowercase().as_str() {
            "markdown" | "content" | "body" => Self::Markdown,
            "metadata" | "frontmatter" => Self::Metadata,
            _ => Self::Full,
        }
    }

    pub fn as_str(self) -> &'static str {
        match self {
            Self::Full => "full",
            Self::Markdown => "markdown",
            Self::Metadata => "metadata",
        }
    }
}

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 title_from_markdown(markdown: &str, slug: &str) -> Option<String> {
    let normalized = markdown.replace("\r\n", "\n");
    if let Some(frontmatter) = normalized.strip_prefix("---\n").and_then(|rest| {
        rest.split_once("\n---\n")
            .map(|(frontmatter, _)| frontmatter)
    }) {
        for line in frontmatter.lines() {
            let trimmed = line.trim();
            if let Some(raw) = trimmed.strip_prefix("title:") {
                let title = raw.trim().trim_matches('"').trim_matches('\'').trim();
                if !title.is_empty() {
                    return Some(title.to_string());
                }
            }
        }
    }

    normalized
        .lines()
        .find_map(|line| {
            line.trim()
                .strip_prefix("# ")
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .map(ToString::to_string)
        })
        .or_else(|| trim_to_option(Some(slug.to_string())))
}

async fn lookup_post_title(ctx: &AppContext, slug: &str) -> Option<String> {
    posts::Entity::find()
        .filter(posts::Column::Slug.eq(slug))
        .one(&ctx.db)
        .await
        .ok()
        .flatten()
        .and_then(|item| item.title)
        .and_then(|value| trim_to_option(Some(value)))
}

pub async fn capture_snapshot_from_markdown(
    ctx: &AppContext,
    actor: Option<&AdminIdentity>,
    slug: &str,
    markdown: &str,
    operation: &str,
    reason: Option<&str>,
    metadata: Option<serde_json::Value>,
) -> Result<post_revisions::Model> {
    let post_title = lookup_post_title(ctx, slug)
        .await
        .or_else(|| title_from_markdown(markdown, slug));

    post_revisions::ActiveModel {
        post_slug: Set(slug.to_string()),
        post_title: Set(post_title),
        operation: Set(operation.to_string()),
        revision_reason: Set(reason.map(ToString::to_string)),
        actor_username: Set(actor.map(|item| item.username.clone())),
        actor_email: Set(actor.and_then(|item| item.email.clone())),
        actor_source: Set(actor.map(|item| item.source.clone())),
        markdown: Set(Some(markdown.replace("\r\n", "\n"))),
        metadata: Set(metadata),
        ..Default::default()
    }
    .insert(&ctx.db)
    .await
    .map_err(Into::into)
}

pub async fn capture_current_snapshot(
    ctx: &AppContext,
    actor: Option<&AdminIdentity>,
    slug: &str,
    operation: &str,
    reason: Option<&str>,
    metadata: Option<serde_json::Value>,
) -> Result<Option<post_revisions::Model>> {
    let Ok((_path, markdown)) = content::read_markdown_document_from_store(ctx, slug).await else {
        return Ok(None);
    };

    capture_snapshot_from_markdown(ctx, actor, slug, &markdown, operation, reason, metadata)
        .await
        .map(Some)
}

pub async fn list_revisions(
    ctx: &AppContext,
    slug: Option<&str>,
    limit: u64,
) -> Result<Vec<post_revisions::Model>> {
    let mut query =
        post_revisions::Entity::find().order_by(post_revisions::Column::CreatedAt, Order::Desc);

    if let Some(slug) = slug.map(str::trim).filter(|value| !value.is_empty()) {
        query = query.filter(post_revisions::Column::PostSlug.eq(slug));
    }

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

pub async fn get_revision(ctx: &AppContext, id: i32) -> Result<post_revisions::Model> {
    post_revisions::Entity::find_by_id(id)
        .one(&ctx.db)
        .await?
        .ok_or(Error::NotFound)
}

pub async fn restore_revision(
    ctx: &AppContext,
    actor: Option<&AdminIdentity>,
    revision_id: i32,
    mode: &str,
) -> Result<post_revisions::Model> {
    let revision = get_revision(ctx, revision_id).await?;
    let slug = revision.post_slug.clone();
    let revision_markdown = revision
        .markdown
        .clone()
        .filter(|value| !value.trim().is_empty())
        .ok_or_else(|| Error::BadRequest("该版本没有可恢复的 Markdown 快照".to_string()))?;
    let restore_mode = RestoreMode::parse(mode);

    let _ = capture_current_snapshot(
        ctx,
        actor,
        &slug,
        "restore_backup",
        Some("恢复前自动备份"),
        Some(serde_json::json!({
            "source_revision_id": revision_id,
            "mode": restore_mode.as_str(),
        })),
    )
    .await?;

    let markdown = match restore_mode {
        RestoreMode::Full => revision_markdown.clone(),
        RestoreMode::Markdown | RestoreMode::Metadata => {
            let (_path, current_markdown) = content::read_markdown_document_from_store(ctx, &slug)
                .await
                .map_err(|_| {
                    Error::BadRequest(
                        "当前文章不存在，无法执行局部恢复，请改用完整恢复".to_string(),
                    )
                })?;
            let virtual_path = content::virtual_markdown_document_path(&slug);
            let revision_post =
                content::parse_markdown_source(&slug, &revision_markdown, &virtual_path)?;
            let current_post =
                content::parse_markdown_source(&slug, &current_markdown, &virtual_path)?;
            let mut merged = current_post.clone();
            match restore_mode {
                RestoreMode::Markdown => {
                    merged.content = revision_post.content;
                }
                RestoreMode::Metadata => {
                    merged.title = revision_post.title;
                    merged.description = revision_post.description;
                    merged.category = revision_post.category;
                    merged.tags = revision_post.tags;
                    merged.post_type = revision_post.post_type;
                    merged.image = revision_post.image;
                    merged.images = revision_post.images;
                    merged.pinned = revision_post.pinned;
                    merged.status = revision_post.status;
                    merged.visibility = revision_post.visibility;
                    merged.publish_at = revision_post.publish_at;
                    merged.unpublish_at = revision_post.unpublish_at;
                    merged.canonical_url = revision_post.canonical_url;
                    merged.noindex = revision_post.noindex;
                    merged.og_image = revision_post.og_image;
                    merged.redirect_from = revision_post.redirect_from;
                    merged.redirect_to = revision_post.redirect_to;
                }
                RestoreMode::Full => unreachable!(),
            }
            content::build_markdown_document(&merged)
        }
    };

    content::write_markdown_document(ctx, &slug, &markdown).await?;

    let _ = capture_snapshot_from_markdown(
        ctx,
        actor,
        &slug,
        &markdown,
        "restore",
        Some("通过版本历史恢复"),
        Some(serde_json::json!({
            "source_revision_id": revision_id,
            "mode": restore_mode.as_str(),
        })),
    )
    .await?;

    Ok(revision)
}
