use crate::AppService;
use crate::error::AppError;
use models::DateTimeUtc;
use models::projects::{MemberRole, project_audit_log};
use sea_orm::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use session::Session;
use uuid::Uuid;

#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct AuditLogParams {
    pub action: String,
    pub details: Option<Value>,
}

use utoipa::__dev::ComposeSchema;
use utoipa::ToSchema;
use utoipa::openapi::schema::{ObjectBuilder, Type};
use utoipa::openapi::{KnownFormat, SchemaFormat};

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct AuditLogResponse {
    pub id: i64,
    pub project_uid: Uuid,
    pub actor_uid: Uuid,
    pub action: String,
    pub details: Option<Value>,
    pub created_at: DateTimeUtc,
}

impl ComposeSchema for AuditLogResponse {
    fn compose(
        _: Vec<utoipa::openapi::RefOr<utoipa::openapi::Schema>>,
    ) -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
        utoipa::openapi::RefOr::T(utoipa::openapi::Schema::Object(
            ObjectBuilder::new()
                .property(
                    "id",
                    ObjectBuilder::new().schema_type(Type::Integer).format(Some(
                        utoipa::openapi::schema::SchemaFormat::KnownFormat(
                            utoipa::openapi::schema::KnownFormat::Int64,
                        ),
                    )),
                )
                .property(
                    "project_uid",
                    ObjectBuilder::new()
                        .schema_type(Type::String)
                        .format(Some(SchemaFormat::KnownFormat(KnownFormat::Uuid))),
                )
                .property(
                    "actor_uid",
                    ObjectBuilder::new()
                        .schema_type(Type::String)
                        .format(Some(SchemaFormat::KnownFormat(KnownFormat::Uuid))),
                )
                .property("action", ObjectBuilder::new().schema_type(Type::String))
                .property("details", ObjectBuilder::new())
                .required("id")
                .required("project_uid")
                .required("actor_uid")
                .required("action")
                .into(),
        ))
    }
}

impl ToSchema for AuditLogResponse {}

#[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
pub struct AuditLogListResponse {
    pub logs: Vec<AuditLogResponse>,
    pub total: u64,
    pub page: u64,
    pub per_page: u64,
}

impl From<project_audit_log::Model> for AuditLogResponse {
    fn from(log: project_audit_log::Model) -> Self {
        AuditLogResponse {
            id: log.id,
            project_uid: log.project,
            actor_uid: log.actor,
            action: log.action,
            details: log.details,
            created_at: log.created_at,
        }
    }
}

impl AppService {
    pub async fn project_log_audit(
        &self,
        project_name: String,
        params: AuditLogParams,
        ctx: &Session,
    ) -> Result<AuditLogResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        let log = project_audit_log::ActiveModel {
            project: Set(project.id),
            actor: Set(user_uid),
            action: Set(params.action),
            details: Set(params.details),
            created_at: Set(chrono::Utc::now()),
            ..Default::default()
        };
        let created_log = log
            .insert(&self.db)
            .await
            .map_err(|_| AppError::UserNotFound)?;

        Ok(AuditLogResponse::from(created_log))
    }

    pub async fn project_get_audit_logs(
        &self,
        project_name: String,
        page: Option<u64>,
        per_page: Option<u64>,
        ctx: &Session,
    ) -> Result<AuditLogListResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        self.utils_check_project_permission(
            &project.id,
            user_uid,
            &[MemberRole::Owner, MemberRole::Admin],
        )
        .await?;
        let page = page.unwrap_or(1);
        let per_page = per_page.unwrap_or(20);

        let logs = project_audit_log::Entity::find()
            .filter(project_audit_log::Column::Project.eq(project.id))
            .order_by_desc(project_audit_log::Column::CreatedAt)
            .paginate(&self.db, per_page)
            .fetch_page(page - 1)
            .await
            .map_err(|_| AppError::UserNotFound)?;

        let total = project_audit_log::Entity::find()
            .filter(project_audit_log::Column::Project.eq(project.id))
            .count(&self.db)
            .await
            .map_err(|_| AppError::UserNotFound)?;

        let logs = logs.into_iter().map(AuditLogResponse::from).collect();

        Ok(AuditLogListResponse {
            logs,
            total,
            page,
            per_page,
        })
    }

    pub async fn project_get_audit_log(
        &self,
        log_id: i64,
        ctx: &Session,
    ) -> Result<AuditLogResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;

        let log = project_audit_log::Entity::find_by_id(log_id)
            .one(&self.db)
            .await
            .map_err(|_| AppError::UserNotFound)?
            .ok_or(AppError::UserNotFound)?;

        self.utils_check_project_permission(
            &log.project,
            user_uid,
            &[MemberRole::Owner, MemberRole::Admin],
        )
        .await?;

        Ok(AuditLogResponse::from(log))
    }
}
