use crate::AppService;
use crate::error::AppError;
use chrono::Utc;
use models::projects::{MemberRole, project_members};
use models::pull_request::{pull_request, pull_request_review_comment};
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
use uuid::Uuid;

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct ReviewCommentCreateRequest {
    pub body: String,
    pub review: Option<Uuid>,
    pub path: Option<String>,
    pub side: Option<String>,
    pub line: Option<i64>,
    pub old_line: Option<i64>,
    /// ID of the parent comment to reply to (null = root comment).
    pub in_reply_to: Option<i64>,
}

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct ReviewCommentUpdateRequest {
    pub body: String,
}

/// Body for replying to an existing review comment thread.
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct ReviewCommentReplyRequest {
    pub body: String,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ReviewCommentResponse {
    pub repo: Uuid,
    pub number: i64,
    pub id: i64,
    pub review: Option<Uuid>,
    pub path: Option<String>,
    pub side: Option<String>,
    pub line: Option<i64>,
    pub old_line: Option<i64>,
    pub body: String,
    pub author: Uuid,
    pub author_username: Option<String>,
    pub resolved: bool,
    pub in_reply_to: Option<i64>,
    pub created_at: chrono::DateTime<Utc>,
    pub updated_at: chrono::DateTime<Utc>,
}

impl From<pull_request_review_comment::Model> for ReviewCommentResponse {
    fn from(c: pull_request_review_comment::Model) -> Self {
        Self {
            repo: c.repo,
            number: c.number,
            id: c.id,
            review: c.review,
            path: c.path,
            side: c.side,
            line: c.line,
            old_line: c.old_line,
            body: c.body,
            author: c.author,
            author_username: None,
            resolved: c.resolved,
            in_reply_to: c.in_reply_to,
            created_at: c.created_at,
            updated_at: c.updated_at,
        }
    }
}

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct ReviewCommentListQuery {
    /// Filter comments by file path (e.g. "src/main.rs").
    pub path: Option<String>,
    /// Filter by resolved status. Omit to return all comments.
    pub resolved: Option<bool>,
    /// If true, only return inline comments (those with a `path` set).
    /// If false, only return general comments (no path).
    /// Omit to return all comments.
    pub file_only: Option<bool>,
}

/// A review comment thread: one root comment plus all its replies.
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ReviewCommentThread {
    pub root: ReviewCommentResponse,
    pub replies: Vec<ReviewCommentResponse>,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ReviewCommentListResponse {
    /// Flat list of all comments (kept for backward compatibility).
    pub comments: Vec<ReviewCommentResponse>,
    /// Comments grouped into threads (root comments with their replies).
    pub threads: Vec<ReviewCommentThread>,
    pub total: i64,
}

impl AppService {
    /// List review comments on a pull request, optionally filtered and grouped into threads.
    pub async fn review_comment_list(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        query: ReviewCommentListQuery,
        ctx: &Session,
    ) -> Result<ReviewCommentListResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let mut stmt = pull_request_review_comment::Entity::find()
            .filter(pull_request_review_comment::Column::Repo.eq(repo.id))
            .filter(pull_request_review_comment::Column::Number.eq(pr_number))
            .order_by_asc(pull_request_review_comment::Column::Path)
            .order_by_asc(pull_request_review_comment::Column::Line)
            .order_by_asc(pull_request_review_comment::Column::CreatedAt);

        if let Some(ref path) = query.path {
            stmt = stmt.filter(pull_request_review_comment::Column::Path.eq(path.clone()));
        }
        if let Some(resolved) = query.resolved {
            stmt = stmt.filter(pull_request_review_comment::Column::Resolved.eq(resolved));
        }
        if query.file_only == Some(true) {
            stmt = stmt.filter(pull_request_review_comment::Column::Path.is_not_null());
        } else if query.file_only == Some(false) {
            stmt = stmt.filter(pull_request_review_comment::Column::Path.is_null());
        }

        let comments = stmt.all(&self.db).await?;

        let total = comments.len() as i64;

        let author_ids: Vec<Uuid> = comments.iter().map(|c| c.author).collect();
        let authors = if author_ids.is_empty() {
            vec![]
        } else {
            models::users::user::Entity::find()
                .filter(models::users::user::Column::Uid.is_in(author_ids))
                .all(&self.db)
                .await?
        };

        let responses: Vec<ReviewCommentResponse> = comments
            .iter()
            .map(|c| {
                let username = authors
                    .iter()
                    .find(|u| u.uid == c.author)
                    .map(|u| u.username.clone());
                ReviewCommentResponse {
                    author_username: username,
                    ..ReviewCommentResponse::from(c.clone())
                }
            })
            .collect();

        // Group into threads: root comments (in_reply_to IS NULL) with their replies.
        let mut threads: Vec<ReviewCommentThread> = Vec::new();

        // Build a map of parent_comment_id → list of reply responses.
        let mut reply_map: std::collections::HashMap<i64, Vec<ReviewCommentResponse>> =
            std::collections::HashMap::new();

        for comment in &responses {
            if let Some(parent_id) = comment.in_reply_to {
                reply_map
                    .entry(parent_id)
                    .or_default()
                    .push(comment.clone());
            }
        }

        // Root comments are those with no parent.
        for comment in &responses {
            if comment.in_reply_to.is_none() {
                let replies = reply_map.remove(&comment.id).unwrap_or_default();
                threads.push(ReviewCommentThread {
                    root: comment.clone(),
                    replies,
                });
            }
        }

        // Sort threads: by file path, then by line number of root comment.
        threads.sort_by(|a, b| {
            let path_cmp = a.root.path.cmp(&b.root.path);
            if path_cmp != std::cmp::Ordering::Equal {
                return path_cmp;
            }
            a.root.line.cmp(&b.root.line)
        });

        Ok(ReviewCommentListResponse {
            comments: responses,
            threads,
            total,
        })
    }

    pub async fn review_comment_create(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        request: ReviewCommentCreateRequest,
        ctx: &Session,
    ) -> Result<ReviewCommentResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        // Verify PR exists
        let pr = pull_request::Entity::find()
            .filter(pull_request::Column::Repo.eq(repo.id))
            .filter(pull_request::Column::Number.eq(pr_number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Pull request not found".to_string()))?;

        if pr.status == models::pull_request::PrStatus::Merged.to_string() {
            return Err(AppError::BadRequest(
                "Cannot comment on a merged pull request".to_string(),
            ));
        }

        // Get next comment id for this PR
        let max_id: Option<Option<i64>> = pull_request_review_comment::Entity::find()
            .filter(pull_request_review_comment::Column::Repo.eq(repo.id))
            .filter(pull_request_review_comment::Column::Number.eq(pr_number))
            .select_only()
            .column_as(pull_request_review_comment::Column::Id.max(), "max_id")
            .into_tuple::<Option<i64>>()
            .one(&self.db)
            .await?;
        let comment_id = max_id.flatten().unwrap_or(0) + 1;

        let now = Utc::now();
        let active = pull_request_review_comment::ActiveModel {
            repo: Set(repo.id),
            number: Set(pr_number),
            id: Set(comment_id),
            review: Set(request.review),
            path: Set(request.path),
            side: Set(request.side),
            line: Set(request.line),
            old_line: Set(request.old_line),
            body: Set(request.body),
            author: Set(user_uid),
            resolved: Set(false),
            in_reply_to: Set(request.in_reply_to),
            created_at: Set(now),
            updated_at: Set(now),
        };
        let model = active.insert(&self.db).await?;

        super::invalidate_pr_cache(&self.cache, repo.id, pr_number).await;

        let username = models::users::user::Entity::find_by_id(user_uid)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username);

        Ok(ReviewCommentResponse {
            author_username: username,
            ..ReviewCommentResponse::from(model)
        })
    }

    pub async fn review_comment_update(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        comment_id: i64,
        request: ReviewCommentUpdateRequest,
        ctx: &Session,
    ) -> Result<ReviewCommentResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let comment =
            pull_request_review_comment::Entity::find_by_id((repo.id, pr_number, comment_id))
                .one(&self.db)
                .await?
                .ok_or(AppError::NotFound("Comment not found".to_string()))?;

        // Permission: author OR admin/owner
        let is_author = comment.author == user_uid;
        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(repo.project))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await?
            .ok_or(AppError::NoPower)?;
        let role = member.scope_role().map_err(|_| AppError::RoleParseError)?;
        let is_admin = role == MemberRole::Admin || role == MemberRole::Owner;

        if !is_author && !is_admin {
            return Err(AppError::NoPower);
        }

        let mut active: pull_request_review_comment::ActiveModel = comment.clone().into();
        active.body = Set(request.body);
        active.updated_at = Set(Utc::now());
        let model = active.update(&self.db).await?;

        super::invalidate_pr_cache(&self.cache, repo.id, pr_number).await;

        let username = models::users::user::Entity::find_by_id(model.author)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username);

        Ok(ReviewCommentResponse {
            author_username: username,
            ..ReviewCommentResponse::from(model)
        })
    }

    pub async fn review_comment_delete(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        comment_id: i64,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let comment =
            pull_request_review_comment::Entity::find_by_id((repo.id, pr_number, comment_id))
                .one(&self.db)
                .await?
                .ok_or(AppError::NotFound("Comment not found".to_string()))?;

        // Permission: author OR admin/owner
        let is_author = comment.author == user_uid;
        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(repo.project))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await?
            .ok_or(AppError::NoPower)?;
        let role = member.scope_role().map_err(|_| AppError::RoleParseError)?;
        let is_admin = role == MemberRole::Admin || role == MemberRole::Owner;

        if !is_author && !is_admin {
            return Err(AppError::NoPower);
        }

        pull_request_review_comment::Entity::delete_by_id((repo.id, pr_number, comment_id))
            .exec(&self.db)
            .await?;

        super::invalidate_pr_cache(&self.cache, repo.id, pr_number).await;

        Ok(())
    }

    /// Mark a review comment (root of a thread) as resolved.
    pub async fn review_comment_resolve(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        comment_id: i64,
        ctx: &Session,
    ) -> Result<ReviewCommentResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let comment =
            pull_request_review_comment::Entity::find_by_id((repo.id, pr_number, comment_id))
                .one(&self.db)
                .await?
                .ok_or(AppError::NotFound("Comment not found".to_string()))?;

        self.check_comment_permission(&repo, &comment, user_uid)
            .await?;

        let mut active: pull_request_review_comment::ActiveModel = comment.clone().into();
        active.resolved = Set(true);
        active.updated_at = Set(Utc::now());
        let model = active.update(&self.db).await?;

        super::invalidate_pr_cache(&self.cache, repo.id, pr_number).await;

        let username = models::users::user::Entity::find_by_id(model.author)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username);

        Ok(ReviewCommentResponse {
            author_username: username,
            ..ReviewCommentResponse::from(model)
        })
    }

    /// Mark a review comment thread as unresolved.
    pub async fn review_comment_unresolve(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        comment_id: i64,
        ctx: &Session,
    ) -> Result<ReviewCommentResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let comment =
            pull_request_review_comment::Entity::find_by_id((repo.id, pr_number, comment_id))
                .one(&self.db)
                .await?
                .ok_or(AppError::NotFound("Comment not found".to_string()))?;

        self.check_comment_permission(&repo, &comment, user_uid)
            .await?;

        let mut active: pull_request_review_comment::ActiveModel = comment.clone().into();
        active.resolved = Set(false);
        active.updated_at = Set(Utc::now());
        let model = active.update(&self.db).await?;

        super::invalidate_pr_cache(&self.cache, repo.id, pr_number).await;

        let username = models::users::user::Entity::find_by_id(model.author)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username);

        Ok(ReviewCommentResponse {
            author_username: username,
            ..ReviewCommentResponse::from(model)
        })
    }

    /// Reply to an existing review comment, creating a threaded reply.
    pub async fn review_comment_reply(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        parent_comment_id: i64,
        request: ReviewCommentReplyRequest,
        ctx: &Session,
    ) -> Result<ReviewCommentResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        // Verify PR exists and is not merged
        let pr = pull_request::Entity::find()
            .filter(pull_request::Column::Repo.eq(repo.id))
            .filter(pull_request::Column::Number.eq(pr_number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Pull request not found".to_string()))?;

        if pr.status == models::pull_request::PrStatus::Merged.to_string() {
            return Err(AppError::BadRequest(
                "Cannot comment on a merged pull request".to_string(),
            ));
        }

        // Verify parent comment exists
        let parent = pull_request_review_comment::Entity::find_by_id((
            repo.id,
            pr_number,
            parent_comment_id,
        ))
        .one(&self.db)
        .await?
        .ok_or(AppError::NotFound("Parent comment not found".to_string()))?;

        // Get next comment id
        let max_id: Option<Option<i64>> = pull_request_review_comment::Entity::find()
            .filter(pull_request_review_comment::Column::Repo.eq(repo.id))
            .filter(pull_request_review_comment::Column::Number.eq(pr_number))
            .select_only()
            .column_as(pull_request_review_comment::Column::Id.max(), "max_id")
            .into_tuple::<Option<i64>>()
            .one(&self.db)
            .await?;
        let comment_id = max_id.flatten().unwrap_or(0) + 1;

        let now = Utc::now();
        let active = pull_request_review_comment::ActiveModel {
            repo: Set(repo.id),
            number: Set(pr_number),
            id: Set(comment_id),
            review: Set(None),
            path: Set(parent.path.clone()),
            side: Set(parent.side.clone()),
            line: Set(parent.line),
            old_line: Set(parent.old_line),
            body: Set(request.body),
            author: Set(user_uid),
            resolved: Set(false),
            in_reply_to: Set(Some(parent.id)),
            created_at: Set(now),
            updated_at: Set(now),
        };
        let model = active.insert(&self.db).await?;

        super::invalidate_pr_cache(&self.cache, repo.id, pr_number).await;

        let username = models::users::user::Entity::find_by_id(user_uid)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username);

        Ok(ReviewCommentResponse {
            author_username: username,
            ..ReviewCommentResponse::from(model)
        })
    }

    async fn check_comment_permission(
        &self,
        repo: &models::repos::repo::Model,
        comment: &pull_request_review_comment::Model,
        user_uid: Uuid,
    ) -> Result<(), AppError> {
        // Authors can always modify their own comments
        if comment.author == user_uid {
            return Ok(());
        }

        // Admins/owners can modify any comment
        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(repo.project))
            .filter(project_members::Column::User.eq(user_uid))
            .one(&self.db)
            .await?
            .ok_or(AppError::NoPower)?;
        let role = member.scope_role().map_err(|_| AppError::RoleParseError)?;
        let is_admin = role == MemberRole::Admin || role == MemberRole::Owner;

        if is_admin {
            Ok(())
        } else {
            Err(AppError::NoPower)
        }
    }
}
