use crate::error::AppError;
use crate::AppService;
use chrono::{DateTime, Utc};
use models::projects::{project_members, Project};
use models::repos::repo::{
    ActiveModel as RepoActiveModel, Column as RepoColumn, Entity as RepoEntity,
};
use models::repos::{Repo, RepoBranch, RepoCommit, RepoStar, RepoTag, RepoWatch};
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use std::collections::HashMap;
use std::path::PathBuf;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema, IntoParams)]
pub struct ProjectRepositoryQuery {
    pub limit: Option<u64>,
    pub cursor: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ProjectRepositoryItem {
    pub uid: Uuid,
    pub repo_name: String,
    pub description: Option<String>,
    pub default_branch: String,
    pub project_name: String,
    pub is_private: bool,
    pub commit_count: i64,
    pub branch_count: i64,
    pub tag_count: i64,
    pub star_count: i64,
    pub watch_count: i64,
    pub last_commit_at: Option<DateTime<Utc>>,
    pub ssh_clone_url: String,
    pub https_clone_url: String,
    pub ai_code_review_enabled: bool,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ProjectRepositoryPagination {
    pub items: Vec<ProjectRepositoryItem>,
    pub cursor: Option<String>,
    pub total: u64,
}

#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct ProjectRepoCreateParams {
    pub repo_name: String,
    pub description: Option<String>,
    /// Default: true. When false, skips bare git init and leaves default_branch empty;
    /// the branch will be auto-detected and set on first push.
    #[serde(default = "default_true")]
    pub init_repo: bool,
    /// Only used when init_repo is true.
    #[serde(default = "default_branch_name")]
    pub default_branch: String,
    #[serde(default)]
    pub is_private: bool,
}

fn default_true() -> bool {
    true
}

fn default_branch_name() -> String {
    "main".to_string()
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ProjectRepoCreateResponse {
    pub uid: Uuid,
    pub repo_name: String,
    pub description: Option<String>,
    pub default_branch: String,
    pub project_name: String,
    pub is_private: bool,
    pub storage_path: String,
    pub created_at: DateTime<Utc>,
}

impl AppService {
    pub async fn project_repo(
        &self,
        _ctx: &Session,
        project_name: String,
        query: ProjectRepositoryQuery,
    ) -> Result<ProjectRepositoryPagination, AppError> {
        let limit = query.limit.unwrap_or(10);

        let project = Project::find()
            .filter(models::projects::project::Column::Name.eq(&project_name))
            .one(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .ok_or(AppError::ProjectNotFound)?;

        let repo_list = Repo::find()
            .filter(models::repos::repo::Column::Project.eq(project.id))
            .order_by_desc(models::repos::repo::Column::UpdatedAt)
            .limit(limit)
            .all(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        let repo_total = Repo::find()
            .filter(models::repos::repo::Column::Project.eq(project.id))
            .count(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        if repo_list.is_empty() {
            return Ok(ProjectRepositoryPagination {
                items: vec![],
                cursor: None,
                total: repo_total,
            });
        }

        let repo_ids: Vec<Uuid> = repo_list.iter().map(|r| r.id).collect();

        let commit_counts: HashMap<Uuid, i64> = RepoCommit::find()
            .select_only()
            .column(models::repos::repo_commit::Column::Repo)
            .column_as(models::repos::repo_commit::Column::Id.count(), "count")
            .filter(models::repos::repo_commit::Column::Repo.is_in(repo_ids.clone()))
            .group_by(models::repos::repo_commit::Column::Repo)
            .into_tuple::<(Uuid, i64)>()
            .all(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .into_iter()
            .collect();

        let branch_counts: HashMap<Uuid, i64> = RepoBranch::find()
            .select_only()
            .column(models::repos::repo_branch::Column::Repo)
            .column_as(models::repos::repo_branch::Column::Repo.count(), "count")
            .filter(models::repos::repo_branch::Column::Repo.is_in(repo_ids.clone()))
            .group_by(models::repos::repo_branch::Column::Repo)
            .into_tuple::<(Uuid, i64)>()
            .all(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .into_iter()
            .collect();

        let tag_counts: HashMap<Uuid, i64> = RepoTag::find()
            .select_only()
            .column(models::repos::repo_tag::Column::Repo)
            .column_as(models::repos::repo_tag::Column::Repo.count(), "count")
            .filter(models::repos::repo_tag::Column::Repo.is_in(repo_ids.clone()))
            .group_by(models::repos::repo_tag::Column::Repo)
            .into_tuple::<(Uuid, i64)>()
            .all(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .into_iter()
            .collect();

        let star_counts: HashMap<Uuid, i64> = RepoStar::find()
            .select_only()
            .column(models::repos::repo_star::Column::Repo)
            .column_as(models::repos::repo_star::Column::User.count(), "count")
            .filter(models::repos::repo_star::Column::Repo.is_in(repo_ids.clone()))
            .group_by(models::repos::repo_star::Column::Repo)
            .into_tuple::<(Uuid, i64)>()
            .all(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .into_iter()
            .collect();

        let watch_counts: HashMap<Uuid, i64> = RepoWatch::find()
            .select_only()
            .column(models::repos::repo_watch::Column::Repo)
            .column_as(models::repos::repo_watch::Column::User.count(), "count")
            .filter(models::repos::repo_watch::Column::Repo.is_in(repo_ids.clone()))
            .group_by(models::repos::repo_watch::Column::Repo)
            .into_tuple::<(Uuid, i64)>()
            .all(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .into_iter()
            .collect();

        let last_commit_times: HashMap<Uuid, Option<DateTime<Utc>>> = {
            let mut map: HashMap<Uuid, Option<DateTime<Utc>>> = HashMap::new();
            for repo_id in &repo_ids {
                let last_commit: Option<models::repos::repo_commit::Model> = RepoCommit::find()
                    .filter(models::repos::repo_commit::Column::Repo.eq(*repo_id))
                    .order_by_desc(models::repos::repo_commit::Column::CreatedAt)
                    .one(&self.db)
                    .await
                    .map_err(|e| AppError::DatabaseError(e.to_string()))?;

                let time = last_commit.map(|c| c.created_at);

                map.insert(*repo_id, time);
            }
            map
        };

        let ssh_domain = self
            .config
            .ssh_domain()
            .unwrap_or_else(|_| "code".to_string());

        let items: Vec<ProjectRepositoryItem> = repo_list
            .into_iter()
            .map(|r| {
                let path = format!("{}/{}", project.name, r.repo_name);
                ProjectRepositoryItem {
                    uid: r.id,
                    repo_name: r.repo_name,
                    description: r.description,
                    default_branch: r.default_branch,
                    project_name: project.name.clone(),
                    is_private: r.is_private,
                    commit_count: *commit_counts.get(&r.id).unwrap_or(&0),
                    branch_count: *branch_counts.get(&r.id).unwrap_or(&0),
                    tag_count: *tag_counts.get(&r.id).unwrap_or(&0),
                    star_count: *star_counts.get(&r.id).unwrap_or(&0),
                    watch_count: *watch_counts.get(&r.id).unwrap_or(&0),
                    last_commit_at: last_commit_times.get(&r.id).and_then(|t| *t),
                    ssh_clone_url: format!("git@{}:{}", ssh_domain, path),
                    https_clone_url: format!("https://{}/{}", ssh_domain, path),
                    ai_code_review_enabled: r.ai_code_review_enabled,
                }
            })
            .collect();

        Ok(ProjectRepositoryPagination {
            items,
            cursor: None,
            total: repo_total,
        })
    }

    pub async fn project_repo_create(
        &self,
        ctx: &Session,
        project_name: String,
        params: ProjectRepoCreateParams,
    ) -> Result<ProjectRepoCreateResponse, AppError> {
        let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;

        // Find project and verify membership
        let project = self
            .utils_find_project_by_name(project_name.clone())
            .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
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .ok_or(AppError::NoPower)?;

        // Check repo name uniqueness within project
        let existing = RepoEntity::find()
            .filter(RepoColumn::Project.eq(project.id))
            .filter(RepoColumn::RepoName.eq(&params.repo_name))
            .one(&self.db)
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        if existing.is_some() {
            return Err(AppError::RepoNameAlreadyExists);
        }

        // Build storage path
        let repos_root = self
            .config
            .repos_root()
            .map_err(|e| AppError::InternalServerError(e.to_string()))?;
        let project_dir: PathBuf = [&repos_root, &project.name].iter().collect();
        let repo_dir: PathBuf = project_dir.join(format!("{}.git", params.repo_name));

        // Only initialize bare git repo if requested
        if params.init_repo {
            crate::git::GitDomain::init_bare(&repo_dir).map_err(AppError::from)?;
        }

        // Insert DB record
        let repo_id = Uuid::now_v7();
        let now = Utc::now();

        // default_branch is only set when init_repo is true; otherwise it stays empty
        // and will be detected on first push via the sync hook
        let default_branch = if params.init_repo {
            params.default_branch.clone()
        } else {
            String::new()
        };
        let default_branch_for_log = default_branch.clone();

        let repo = RepoActiveModel {
            id: Set(repo_id),
            repo_name: Set(params.repo_name.clone()),
            project: Set(project.id),
            description: Set(params.description.clone()),
            default_branch: Set(default_branch),
            is_private: Set(params.is_private),
            storage_path: Set(repo_dir.to_string_lossy().to_string()),
            created_by: Set(user_uid),
            created_at: Set(now),
            updated_at: Set(now),
            ai_code_review_enabled: Set(false),
        };

        let repo = repo.insert(&self.db).await?;

        let _ = self
            .project_log_activity(
                project.id,
                Some(repo.id),
                user_uid,
                super::activity::ActivityLogParams {
                    event_type: "repo_create".to_string(),
                    title: format!("{} created repository '{}'", user_uid, params.repo_name),
                    repo_id: Some(repo.id),
                    content: params.description.clone(),
                    event_id: Some(repo.id),
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({
                        "repo_name": params.repo_name,
                        "default_branch": default_branch_for_log,
                        "is_private": params.is_private,
                        "init_repo": params.init_repo,
                    })),
                    is_private: false,
                },
            )
            .await;

        Ok(ProjectRepoCreateResponse {
            uid: repo.id,
            repo_name: repo.repo_name,
            description: repo.description,
            default_branch: repo.default_branch,
            project_name: project.name,
            is_private: repo.is_private,
            storage_path: repo.storage_path,
            created_at: repo.created_at,
        })
    }
}
