use axum::{
    extract::{Path, State},
    http::HeaderMap,
};
use loco_rs::prelude::*;
use sea_orm::{EntityTrait, QueryOrder, Set};
use serde::{Deserialize, Serialize};

use crate::{
    controllers::admin::{check_auth, resolve_admin_identity},
    models::_entities::reviews::{self, Entity as ReviewEntity},
    services::{admin_audit, storage},
};

fn is_public_review_status(status: Option<&str>) -> bool {
    matches!(
        status
            .unwrap_or_default()
            .trim()
            .to_ascii_lowercase()
            .as_str(),
        "published" | "completed" | "done"
    )
}

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

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UpdateReviewRequest {
    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<Vec<String>>,
    pub cover: Option<String>,
    pub link_url: Option<String>,
}

pub async fn list(headers: HeaderMap, State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
    let include_private = resolve_admin_identity(&headers).is_some();
    let reviews = ReviewEntity::find()
        .order_by_desc(reviews::Column::CreatedAt)
        .all(&ctx.db)
        .await?
        .into_iter()
        .filter(|review| include_private || is_public_review_status(review.status.as_deref()))
        .collect::<Vec<_>>();

    format::json(reviews)
}

pub async fn get_one(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<impl IntoResponse> {
    let include_private = resolve_admin_identity(&headers).is_some();
    let review = ReviewEntity::find_by_id(id).one(&ctx.db).await?;

    match review {
        Some(r) if include_private || is_public_review_status(r.status.as_deref()) => {
            format::json(r)
        }
        Some(_) => Err(Error::NotFound),
        None => Err(Error::NotFound),
    }
}

pub async fn create(
    headers: HeaderMap,
    State(ctx): State<AppContext>,
    Json(req): Json<CreateReviewRequest>,
) -> Result<impl IntoResponse> {
    let actor = check_auth(&headers)?;
    let new_review = reviews::ActiveModel {
        title: Set(Some(req.title)),
        review_type: Set(Some(req.review_type)),
        rating: Set(Some(req.rating)),
        review_date: Set(Some(req.review_date)),
        status: Set(Some(req.status)),
        description: Set(Some(req.description)),
        tags: Set(Some(serde_json::to_string(&req.tags).unwrap_or_default())),
        cover: Set(Some(req.cover)),
        link_url: Set(req.link_url.and_then(|value| {
            let trimmed = value.trim().to_string();
            (!trimmed.is_empty()).then_some(trimmed)
        })),
        ..Default::default()
    };

    let review = new_review.insert(&ctx.db).await?;
    admin_audit::log_event(
        &ctx,
        Some(&actor),
        "review.create",
        "review",
        Some(review.id.to_string()),
        review.title.clone(),
        None,
    )
    .await?;
    format::json(review)
}

pub async fn update(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    Json(req): Json<UpdateReviewRequest>,
) -> Result<impl IntoResponse> {
    let actor = check_auth(&headers)?;
    let review = ReviewEntity::find_by_id(id).one(&ctx.db).await?;

    let Some(existing_review) = review else {
        return Err(Error::NotFound);
    };
    let old_cover = existing_review.cover.clone();
    let mut review = existing_review.into_active_model();

    if let Some(title) = req.title {
        review.title = Set(Some(title));
    }
    if let Some(review_type) = req.review_type {
        review.review_type = Set(Some(review_type));
    }
    if let Some(rating) = req.rating {
        review.rating = Set(Some(rating));
    }
    if let Some(review_date) = req.review_date {
        review.review_date = Set(Some(review_date));
    }
    if let Some(status) = req.status {
        review.status = Set(Some(status));
    }
    if let Some(description) = req.description {
        review.description = Set(Some(description));
    }
    if let Some(tags) = req.tags {
        review.tags = Set(Some(serde_json::to_string(&tags).unwrap_or_default()));
    }
    let mut next_cover = old_cover.clone();
    if let Some(cover) = req.cover {
        next_cover = Some(cover.clone());
        review.cover = Set(Some(cover));
    }
    if let Some(link_url) = req.link_url {
        let trimmed = link_url.trim().to_string();
        review.link_url = Set((!trimmed.is_empty()).then_some(trimmed));
    }

    let review = review.update(&ctx.db).await?;
    if let Some(old_cover) = old_cover
        .filter(|old| Some(old.clone()) != next_cover)
        .filter(|old| !old.trim().is_empty())
    {
        if let Err(error) = storage::delete_managed_url(&ctx, &old_cover).await {
            tracing::warn!("failed to cleanup replaced review cover: {error}");
        }
    }
    admin_audit::log_event(
        &ctx,
        Some(&actor),
        "review.update",
        "review",
        Some(review.id.to_string()),
        review.title.clone(),
        None,
    )
    .await?;
    format::json(review)
}

pub async fn remove(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<impl IntoResponse> {
    let actor = check_auth(&headers)?;
    let review = ReviewEntity::find_by_id(id).one(&ctx.db).await?;

    match review {
        Some(r) => {
            let cover = r.cover.clone();
            let title = r.title.clone();
            r.delete(&ctx.db).await?;
            if let Some(cover) = cover.filter(|value| !value.trim().is_empty()) {
                if let Err(error) = storage::delete_managed_url(&ctx, &cover).await {
                    tracing::warn!("failed to cleanup deleted review cover: {error}");
                }
            }
            admin_audit::log_event(
                &ctx,
                Some(&actor),
                "review.delete",
                "review",
                Some(id.to_string()),
                title,
                None,
            )
            .await?;
            format::empty()
        }
        None => Err(Error::NotFound),
    }
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("/api/reviews")
        .add("/", get(list).post(create))
        .add("/{id}", get(get_one).put(update).delete(remove))
}
