use crate::AppService;
use crate::error::AppError;
use crate::project::activity::ActivityLogParams;
use chrono::Utc;
use models::pull_request::{PrStatus, pull_request};
use models::repos::repo;
use models::users::user;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
use uuid::Uuid;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum MergeStrategy {
    MergeCommit,
    Squash,
    Rebase,
}

impl Default for MergeStrategy {
    fn default() -> Self {
        MergeStrategy::MergeCommit
    }
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct MergeAnalysisResponse {
    pub can_fast_forward: bool,
    pub is_up_to_date: bool,
    pub is_normal: bool,
    pub analysis_flags: Vec<String>,
    /// Strategies supported given the current state of the PR.
    pub supported_strategies: Vec<String>,
}

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct MergeRequest {
    #[serde(default)]
    pub fast_forward: bool,
    #[serde(default)]
    pub strategy: MergeStrategy,
    #[serde(default = "default_merge_message")]
    pub message: String,
}

fn default_merge_message() -> String {
    "Merge pull request".to_string()
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct MergeResponse {
    pub repo: Uuid,
    pub number: i64,
    pub status: String,
    pub merged_by: Uuid,
    pub merged_at: chrono::DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct MergeConflictFile {
    pub path: String,
    pub status: String,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct MergeConflictResponse {
    pub has_conflicts: bool,
    pub conflicted_files: Vec<MergeConflictFile>,
}

fn resolve_ref_name(name: &str) -> String {
    if name.starts_with("refs/") {
        name.to_string()
    } else if name.contains('/') {
        format!("refs/heads/{}", name)
    } else {
        format!("refs/heads/{}", name)
    }
}

impl AppService {
    /// Analyze merge readiness of a pull request.
    pub async fn merge_analysis(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        ctx: &Session,
    ) -> Result<MergeAnalysisResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        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()))?;

        let domain = git::GitDomain::from_model(repo)?;

        let head_ref_name = resolve_ref_name(&pr.head);
        let head_oid = domain
            .ref_target(&head_ref_name)?
            .ok_or_else(|| AppError::BadRequest("Head ref has no OID".to_string()))?;

        let (analysis, _pref) = domain.merge_analysis_for_ref(&pr.base, &head_oid)?;

        let mut flags = Vec::new();
        if analysis.is_fast_forward {
            flags.push("fast_forward".to_string());
        }
        if analysis.is_up_to_date {
            flags.push("up_to_date".to_string());
        }
        if analysis.is_normal {
            flags.push("normal".to_string());
        }

        // Determine supported strategies.
        // All three strategies are always available for open PRs.
        let supported_strategies = if analysis.is_up_to_date {
            // Already merged — no strategies available
            vec![]
        } else {
            vec![
                "mergecommit".to_string(),
                "squash".to_string(),
                "rebase".to_string(),
            ]
        };

        Ok(MergeAnalysisResponse {
            can_fast_forward: analysis.is_fast_forward,
            is_up_to_date: analysis.is_up_to_date,
            is_normal: analysis.is_normal,
            analysis_flags: flags,
            supported_strategies,
        })
    }

    pub async fn merge_conflict_check(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        ctx: &Session,
    ) -> Result<MergeConflictResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        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()))?;

        let domain = git::GitDomain::from_model(repo)?;

        let head_ref_name = resolve_ref_name(&pr.head);
        let head_oid = domain
            .ref_target(&head_ref_name)?
            .ok_or_else(|| AppError::BadRequest("Head ref has no OID".to_string()))?;

        let (analysis, _pref) = domain.merge_analysis_for_ref(&pr.base, &head_oid)?;

        let has_conflicts =
            !analysis.is_fast_forward && !analysis.is_up_to_date && domain.merge_is_conflicted();

        if has_conflicts {
            let conflicted_files = self.get_conflicted_files(&domain)?;
            Ok(MergeConflictResponse {
                has_conflicts: true,
                conflicted_files,
            })
        } else {
            Ok(MergeConflictResponse {
                has_conflicts: false,
                conflicted_files: vec![],
            })
        }
    }

    /// ONLY admin/owner of the target repo can merge.
    pub async fn merge_execute(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        request: MergeRequest,
        ctx: &Session,
    ) -> Result<MergeResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;

        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 == PrStatus::Merged.to_string() {
            return Err(AppError::BadRequest(
                "Pull request is already merged".to_string(),
            ));
        }
        if pr.status == PrStatus::Closed.to_string() {
            return Err(AppError::BadRequest(
                "Cannot merge a closed pull request".to_string(),
            ));
        }

        let protection = self.branch_protection_find(repo.id, &pr.base).await?;

        if let Some(ref rule) = protection {
            if rule.forbid_merge {
                return Err(AppError::Forbidden(format!(
                    "Branch '{}' is protected: merges are forbidden",
                    pr.base
                )));
            }

            if rule.required_approvals > 0 {
                let approval_check = self
                    .branch_protection_check_approvals(
                        namespace.clone(),
                        repo_name.clone(),
                        pr_number,
                        ctx,
                    )
                    .await?;
                if !approval_check.enough_approvals {
                    return Err(AppError::Forbidden(format!(
                        "Branch '{}' requires {} approval(s), but only {} found",
                        pr.base, rule.required_approvals, approval_check.approvals
                    )));
                }
            }

            if rule.require_linear_history && request.strategy == MergeStrategy::MergeCommit {
                return Err(AppError::Forbidden(format!(
                    "Branch '{}' requires linear history: merge commits are not allowed, use squash or rebase instead",
                    pr.base
                )));
            }
        }

        let domain = git::GitDomain::from_model(repo.clone())?;

        let head_ref_name = resolve_ref_name(&pr.head);
        let head_oid = domain
            .ref_target(&head_ref_name)?
            .ok_or_else(|| AppError::BadRequest("Head ref has no OID".to_string()))?;
        let base_oid = domain
            .ref_target(&resolve_ref_name(&pr.base))?
            .ok_or_else(|| AppError::BadRequest("Base ref has no OID".to_string()))?;

        let (analysis, _pref) = domain.merge_analysis_for_ref(&pr.base, &head_oid)?;

        if !analysis.is_fast_forward && !analysis.is_up_to_date && domain.merge_is_conflicted() {
            return Err(AppError::BadRequest(
                "Pull request has merge conflicts".to_string(),
            ));
        }

        // Build merge commit message
        let merge_msg = if request.message == default_merge_message() {
            format!("{} (#{})\n\n{}", pr.title, pr_number, pr.title)
        } else {
            request.message
        };

        // Get author signature for merge commit
        let sig = domain.commit_default_signature()?;
        let committer = sig.clone();

        if analysis.is_fast_forward && request.fast_forward {
            // Fast-forward: move base ref forward to head
            let base_ref_name = resolve_ref_name(&pr.base);
            domain.ref_update(&base_ref_name, head_oid.clone(), None, None)?;
        } else {
            match request.strategy {
                MergeStrategy::MergeCommit => {
                    domain.merge_commits(&base_oid, &head_oid, None)?;

                    // Write the merge commit from the merge index
                    let merge_oid = domain.commit_create_from_index(
                        None,
                        &sig,
                        &committer,
                        &merge_msg,
                        &[base_oid.clone(), head_oid.clone()],
                    )?;
                    let base_ref_name = resolve_ref_name(&pr.base);
                    domain.ref_update(&base_ref_name, merge_oid, None, None)?;
                    let _ = domain.merge_abort();
                }
                MergeStrategy::Squash => {
                    // Squash all commits from source branch into one on top of base
                    let squash_oid = domain.squash_commits(&base_oid, &pr.head)?;
                    let base_ref_name = resolve_ref_name(&pr.base);
                    domain.ref_update(&base_ref_name, squash_oid, None, None)?;
                }
                MergeStrategy::Rebase => {
                    // Rebase source commits onto base
                    let rebase_oid = domain.rebase_commits(&base_oid, &head_oid)?;
                    let base_ref_name = resolve_ref_name(&pr.base);
                    domain.ref_update(&base_ref_name, rebase_oid, None, None)?;
                }
            }
        }

        let now = Utc::now();

        let mut active: pull_request::ActiveModel = pr.clone().into();
        active.status = Set(PrStatus::Merged.to_string());
        active.merged_by = Set(Some(user_uid));
        active.merged_at = Set(Some(now));
        active.updated_at = Set(now);
        let merged_model = active.update(&self.db).await?;

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

        let actor_username = user::Entity::find_by_id(user_uid)
            .one(&self.db)
            .await
            .ok()
            .flatten()
            .map(|u| u.username)
            .unwrap_or_default();
        let _ = self
            .project_log_activity(
                repo.project,
                Some(repo.id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "pr_merge".to_string(),
                    title: format!("{} merged pull request #{}", actor_username, pr_number),
                    repo_id: Some(repo.id),
                    content: Some(merged_model.title),
                    event_id: None,
                    event_sub_id: Some(pr_number),
                    metadata: Some(serde_json::json!({
                        "base": pr.clone().base,
                        "head": pr.head.clone(),
                    })),
                    is_private: false,
                },
            )
            .await;

        Ok(MergeResponse {
            repo: repo.id,
            number: pr_number,
            status: PrStatus::Merged.to_string(),
            merged_by: user_uid,
            merged_at: now,
        })
    }

    pub async fn merge_abort(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace, repo_name, ctx)
            .await?;

        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()))?;

        let domain = git::GitDomain::from_model(repo.clone())?;
        domain.merge_abort()?;

        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                repo.project,
                Some(repo.id),
                user_uid,
                ActivityLogParams {
                    event_type: "pr_merge_abort".to_string(),
                    title: format!("{} aborted merge for PR #{}", user_uid, pr_number),
                    repo_id: Some(repo.id),
                    content: None,
                    event_id: None,
                    event_sub_id: Some(pr_number),
                    metadata: Some(serde_json::json!({
                        "pr_number": pr_number,
                    })),
                    is_private: false,
                },
            )
            .await;

        Ok(())
    }

    pub async fn merge_is_in_progress(
        &self,
        namespace: String,
        repo_name: String,
        pr_number: i64,
        ctx: &Session,
    ) -> Result<bool, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        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()))?;

        let domain = git::GitDomain::from_model(repo)?;
        Ok(domain.merge_is_in_progress())
    }

    fn get_conflicted_files(
        &self,
        domain: &git::GitDomain,
    ) -> Result<Vec<MergeConflictFile>, AppError> {
        let index = domain
            .repo()
            .index()
            .map_err(|e| AppError::InternalServerError(e.to_string()))?;

        let files = match index.conflicts() {
            Ok(conflicts) => conflicts
                .filter_map(|result| result.ok())
                .filter_map(|conflict| {
                    conflict.our.as_ref().map(|entry| MergeConflictFile {
                        path: String::from_utf8_lossy(&entry.path).to_string(),
                        status: "both_modified".to_string(),
                    })
                })
                .collect(),
            Err(_) => vec![],
        };

        Ok(files)
    }
}
