use crate::AppService;
use crate::error::AppError;
use crate::project::activity::ActivityLogParams;
use chrono::Utc;
use models::issues::{issue, issue_pull_request};
use models::projects::project_members;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
use uuid::Uuid;

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct IssueLinkPullRequestRequest {
    pub repo: Uuid,
    pub number: i64,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct IssuePullRequestResponse {
    pub issue: Uuid,
    pub repo: Uuid,
    pub number: i64,
    pub relation_at: chrono::DateTime<Utc>,
}

impl AppService {
    /// List pull requests linked to an issue.
    pub async fn issue_pull_request_list(
        &self,
        project_name: String,
        issue_number: i64,
        ctx: &Session,
    ) -> Result<Vec<IssuePullRequestResponse>, AppError> {
        let project = self.utils_find_project_by_name(project_name).await?;
        if let Some(uid) = ctx.user() {
            self.check_project_access(project.id, uid).await?;
        }

        let issue = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::Number.eq(issue_number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Issue not found".to_string()))?;

        let prs = issue_pull_request::Entity::find()
            .filter(issue_pull_request::Column::Issue.eq(issue.id))
            .all(&self.db)
            .await?;

        let responses: Vec<IssuePullRequestResponse> = prs
            .into_iter()
            .map(|pr| IssuePullRequestResponse {
                issue: pr.issue,
                repo: pr.repo,
                number: pr.number,
                relation_at: pr.relation_at,
            })
            .collect();

        Ok(responses)
    }

    /// Link a pull request to an issue.
    pub async fn issue_pull_request_link(
        &self,
        project_name: String,
        issue_number: i64,
        request: IssueLinkPullRequestRequest,
        ctx: &Session,
    ) -> Result<IssuePullRequestResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        let _member = 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_or(AppError::NoPower)?;

        let issue = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::Number.eq(issue_number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Issue not found".to_string()))?;

        let existing = issue_pull_request::Entity::find()
            .filter(issue_pull_request::Column::Issue.eq(issue.id))
            .filter(issue_pull_request::Column::Repo.eq(request.repo))
            .filter(issue_pull_request::Column::Number.eq(request.number))
            .one(&self.db)
            .await?;
        if existing.is_some() {
            return Err(AppError::BadRequest(
                "PR already linked to this issue".to_string(),
            ));
        }

        let now = Utc::now();
        let active = issue_pull_request::ActiveModel {
            issue: Set(issue.id),
            repo: Set(request.repo),
            number: Set(request.number),
            relation_at: Set(now),
            ..Default::default()
        };
        let model = active.insert(&self.db).await?;

        self.invalidate_issue_cache(project.id, issue_number).await;

        let response = Ok(IssuePullRequestResponse {
            issue: model.issue,
            repo: model.repo,
            number: model.number,
            relation_at: model.relation_at,
        });

        let _ = self
            .project_log_activity(
                project.id,
                Some(model.repo),
                user_uid,
                ActivityLogParams {
                    event_type: "pr_issue_link".to_string(),
                    title: format!(
                        "{} linked PR #{} to issue #{}",
                        user_uid, request.number, issue_number
                    ),
                    repo_id: Some(model.repo),
                    content: None,
                    event_id: Some(model.issue),
                    event_sub_id: Some(issue_number),
                    metadata: Some(serde_json::json!({
                        "pr_number": request.number,
                        "pr_repo": request.repo,
                    })),
                    is_private: false,
                },
            )
            .await;

        response
    }

    /// Unlink a pull request from an issue.
    pub async fn issue_pull_request_unlink(
        &self,
        project_name: String,
        issue_number: i64,
        repo_id: Uuid,
        pr_number: i64,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let project = self.utils_find_project_by_name(project_name).await?;

        let _member = 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_or(AppError::NoPower)?;

        let issue = issue::Entity::find()
            .filter(issue::Column::Project.eq(project.id))
            .filter(issue::Column::Number.eq(issue_number))
            .one(&self.db)
            .await?
            .ok_or(AppError::NotFound("Issue not found".to_string()))?;

        issue_pull_request::Entity::delete_many()
            .filter(issue_pull_request::Column::Issue.eq(issue.id))
            .filter(issue_pull_request::Column::Repo.eq(repo_id))
            .filter(issue_pull_request::Column::Number.eq(pr_number))
            .exec(&self.db)
            .await?;

        self.invalidate_issue_cache(project.id, issue_number).await;

        let _ = self
            .project_log_activity(
                project.id,
                Some(repo_id),
                user_uid,
                ActivityLogParams {
                    event_type: "pr_issue_unlink".to_string(),
                    title: format!(
                        "{} unlinked PR #{} from issue #{}",
                        user_uid, pr_number, issue_number
                    ),
                    repo_id: Some(repo_id),
                    content: None,
                    event_id: Some(issue.id),
                    event_sub_id: Some(issue_number),
                    metadata: Some(serde_json::json!({
                        "pr_number": pr_number,
                        "pr_repo": repo_id,
                    })),
                    is_private: false,
                },
            )
            .await;
        Ok(())
    }
}
