use crate::AppService;
use crate::error::AppError;
use models::DateTimeUtc;
use models::projects::MemberRole;
use models::projects::project_activity;
use models::projects::project_members;
use models::repos::repo;
use models::users::user;
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 ActivityParams {
    pub event_type: Option<String>,
    pub start_date: Option<String>,
    pub end_date: Option<String>,
}

#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct ActivityLogParams {
    pub event_type: String,
    pub title: String,
    pub repo_id: Option<Uuid>,
    pub content: Option<String>,
    pub event_id: Option<Uuid>,
    pub event_sub_id: Option<i64>,
    pub metadata: Option<Value>,
    pub is_private: bool,
}

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

/// Role-based visibility level for an activity.
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ActivityVisibility {
    /// Visible to all project members (public activities).
    Public,
    /// Visible only to admins and owners (private activities).
    Private,
}

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ActivityLogResponse {
    pub id: i64,
    pub project_uid: Uuid,
    pub repo_uid: Option<Uuid>,
    pub actor_uid: Uuid,
    pub event_type: String,
    pub event_id: Option<Uuid>,
    pub event_sub_id: Option<i64>,
    pub title: String,
    pub content: Option<String>,
    pub metadata: Option<Value>,
    pub is_private: bool,
    /// Visibility level based on `is_private` flag.
    pub visibility: ActivityVisibility,
    pub created_at: DateTimeUtc,
}

impl ActivityLogResponse {
    /// Apply role-based content filtering.
    /// Admins/owners see everything.
    /// Members see redacted content/metadata for sensitive event types.
    fn apply_role_filter(self, user_role: &Option<MemberRole>) -> ActivityLogResponse {
        let is_admin = matches!(user_role, Some(MemberRole::Owner) | Some(MemberRole::Admin));

        let is_sensitive = matches!(
            self.event_type.as_str(),
            "member_role_change"
                | "member_remove"
                | "member_invite"
                | "invitation_cancelled"
                | "join_request_approve"
                | "join_request_reject"
                | "join_request_cancel"
        );

        if !is_admin && is_sensitive {
            return ActivityLogResponse {
                content: None,
                metadata: None,
                ..self
            };
        }
        self
    }
}

impl ComposeSchema for ActivityLogResponse {
    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(
                    "repo_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("event_type", ObjectBuilder::new().schema_type(Type::String))
                .property("title", ObjectBuilder::new().schema_type(Type::String))
                .property("content", ObjectBuilder::new().schema_type(Type::String))
                .property(
                    "is_private",
                    ObjectBuilder::new().schema_type(Type::Boolean),
                )
                .property("visibility", ObjectBuilder::new().schema_type(Type::String))
                .property("created_at", ObjectBuilder::new().schema_type(Type::String))
                .required("id")
                .required("project_uid")
                .required("actor_uid")
                .required("event_type")
                .required("title")
                .required("is_private")
                .required("visibility")
                .required("created_at")
                .into(),
        ))
    }
}

impl ToSchema for ActivityLogResponse {}

#[derive(Serialize, Deserialize, Clone, Debug, utoipa::ToSchema)]
pub struct ActivityLogListResponse {
    pub logs: Vec<ActivityLogResponse>,
    pub total: u64,
    pub page: u64,
    pub per_page: u64,
    /// The current user's role in the project.
    pub user_role: Option<MemberRole>,
}

impl From<project_activity::Model> for ActivityLogResponse {
    fn from(log: project_activity::Model) -> Self {
        ActivityLogResponse {
            id: log.id,
            project_uid: log.project,
            repo_uid: log.repo,
            actor_uid: log.actor,
            event_type: log.event_type,
            event_id: log.event_id,
            event_sub_id: log.event_sub_id,
            title: log.title,
            content: log.content,
            metadata: log.metadata,
            is_private: log.is_private,
            visibility: if log.is_private {
                ActivityVisibility::Private
            } else {
                ActivityVisibility::Public
            },
            created_at: log.created_at,
        }
    }
}

impl AppService {
    pub async fn project_log_activity(
        &self,
        project_id: Uuid,
        repo_id: Option<Uuid>,
        actor_uid: Uuid,
        params: ActivityLogParams,
    ) -> Result<ActivityLogResponse, AppError> {
        let actor_username = user::Entity::find()
            .filter(user::Column::Uid.eq(actor_uid))
            .one(&self.db)
            .await
            .ok()
            .and_then(|u| u.map(|x| x.username));

        let repo_name = match repo_id {
            Some(rid) => repo::Entity::find()
                .filter(repo::Column::Id.eq(rid))
                .one(&self.db)
                .await
                .ok()
                .and_then(|r| r.map(|x| x.repo_name)),
            None => None,
        };

        // Merge username and repo_name into metadata
        let metadata = {
            let mut meta = params.metadata.unwrap_or_default();
            if let Some(obj) = meta.as_object_mut() {
                if let Some(ref username) = actor_username {
                    obj.insert("username".to_string(), serde_json::json!(username));
                }
                if let Some(ref name) = repo_name {
                    obj.insert("repo_name".to_string(), serde_json::json!(name));
                }
            }
            meta
        };

        let log = project_activity::ActiveModel {
            project: Set(project_id),
            repo: Set(repo_id),
            actor: Set(actor_uid),
            event_type: Set(params.event_type),
            event_id: Set(params.event_id),
            event_sub_id: Set(params.event_sub_id),
            title: Set(params.title),
            content: Set(params.content),
            metadata: Set(Some(metadata)),
            is_private: Set(params.is_private),
            created_at: Set(chrono::Utc::now()),
            ..Default::default()
        };
        let created_log = log
            .insert(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(ActivityLogResponse::from(created_log))
    }

    /// Get project activity feed with pagination and filtering.
    /// Only returns activities the user has permission to see.
    pub async fn project_get_activities(
        &self,
        project_name: String,
        page: Option<u64>,
        per_page: Option<u64>,
        params: Option<ActivityParams>,
        ctx: &Session,
    ) -> Result<ActivityLogListResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        // Check if user has access to the project
        self.check_project_access(project.id, user_uid).await?;

        // Get user's role in the project
        let user_role = match project_members::Entity::find()
            .filter(project_members::Column::Project.eq(project.id))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await
        {
            Ok(Some(m)) => m.scope_role().ok(),
            Ok(None) => None,
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project member for activity log: {}",
                    e
                );
                None
            }
        };

        let is_admin = matches!(user_role, Some(MemberRole::Owner) | Some(MemberRole::Admin));

        let page = page.unwrap_or(1);
        let per_page = per_page.unwrap_or(20);

        // Build the query
        let mut query = project_activity::Entity::find()
            .filter(project_activity::Column::Project.eq(project.id))
            .order_by_desc(project_activity::Column::CreatedAt);

        // Non-admin members can only see public activities
        if !is_admin {
            query = query.filter(project_activity::Column::IsPrivate.eq(false));
        }

        // Apply filters if provided
        if let Some(ref p) = params {
            if let Some(ref event_type) = p.event_type {
                query = query.filter(project_activity::Column::EventType.eq(event_type.clone()));
            }
            if let Some(ref start_date) = p.start_date {
                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(start_date) {
                    query = query.filter(
                        project_activity::Column::CreatedAt.gte(dt.with_timezone(&chrono::Utc)),
                    );
                }
            }
            if let Some(ref end_date) = p.end_date {
                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(end_date) {
                    query = query.filter(
                        project_activity::Column::CreatedAt.lte(dt.with_timezone(&chrono::Utc)),
                    );
                }
            }
        }

        let total = query
            .clone()
            .count(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let logs = query
            .paginate(&self.db, per_page)
            .fetch_page(page - 1)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        // Apply role-based content filtering (redact sensitive fields for non-admin members)
        let logs: Vec<ActivityLogResponse> = logs
            .into_iter()
            .map(|log| ActivityLogResponse::from(log).apply_role_filter(&user_role))
            .collect();

        // Enrich metadata with actor username and repo_name
        let actor_uids: Vec<Uuid> = logs.iter().map(|l| l.actor_uid).collect();
        let username_map = if actor_uids.is_empty() {
            std::collections::HashMap::new()
        } else {
            let users = user::Entity::find()
                .filter(user::Column::Uid.is_in(actor_uids))
                .all(&self.db)
                .await?;
            users.into_iter().map(|u| (u.uid, u.username)).collect()
        };

        let repo_uids: Vec<Uuid> = logs.iter().filter_map(|l| l.repo_uid).collect();
        let repo_name_map = if repo_uids.is_empty() {
            std::collections::HashMap::new()
        } else {
            let repos = repo::Entity::find()
                .filter(repo::Column::Id.is_in(repo_uids))
                .all(&self.db)
                .await?;
            repos.into_iter().map(|r| (r.id, r.repo_name)).collect()
        };

        let logs: Vec<ActivityLogResponse> = logs
            .into_iter()
            .map(|mut log| {
                let meta = log
                    .metadata
                    .get_or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
                if let Some(obj) = meta.as_object_mut() {
                    if let Some(username) = username_map.get(&log.actor_uid) {
                        obj.insert("username".to_string(), serde_json::json!(username));
                    }
                    if let Some(ref repo_uid) = log.repo_uid {
                        if let Some(repo_name) = repo_name_map.get(repo_uid) {
                            obj.insert("repo_name".to_string(), serde_json::json!(repo_name));
                        }
                    }
                }
                log
            })
            .collect();

        Ok(ActivityLogListResponse {
            logs,
            total,
            page,
            per_page,
            user_role,
        })
    }

    /// Get a single activity log by ID.
    pub async fn project_get_activity(
        &self,
        activity_id: i64,
        ctx: &Session,
    ) -> Result<ActivityLogResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;

        let log = project_activity::Entity::find_by_id(activity_id)
            .one(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .ok_or(AppError::NotFound("Activity not found".to_string()))?;

        // Check if user has access to the project
        self.check_project_access(log.project, user_uid).await?;

        // Enrich metadata with actor username and repo_name
        let mut log = ActivityLogResponse::from(log);
        if let Ok(Some(user_record)) = user::Entity::find()
            .filter(user::Column::Uid.eq(log.actor_uid))
            .one(&self.db)
            .await
        {
            if let Some(ref mut meta) = log.metadata {
                if let Some(obj) = meta.as_object_mut() {
                    obj.insert(
                        "username".to_string(),
                        serde_json::json!(user_record.username),
                    );
                }
            } else {
                log.metadata = Some(serde_json::json!({ "username": user_record.username }));
            }
        }

        if let Some(ref repo_uid) = log.repo_uid {
            if let Ok(Some(repo_record)) = repo::Entity::find()
                .filter(repo::Column::Id.eq(*repo_uid))
                .one(&self.db)
                .await
            {
                if let Some(ref mut meta) = log.metadata {
                    if let Some(obj) = meta.as_object_mut() {
                        obj.insert(
                            "repo_name".to_string(),
                            serde_json::json!(repo_record.repo_name),
                        );
                    }
                } else {
                    log.metadata = Some(serde_json::json!({ "repo_name": repo_record.repo_name }));
                }
            }
        }

        Ok(log)
    }
}
