use crate::AppService;
use crate::error::AppError;
use chrono::{DateTime, Utc};
use db::database::AppDatabase;
use models::issues::issue;
use models::projects::{project, project_members};
use models::repos::repo;
use models::users::user;
use sea_orm::*;
use sea_query::{Expr as SqExpr, extension::postgres::PgExpr};
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
use uuid::Uuid;

// ─── Request / Response types ────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
pub struct SearchQuery {
    /// Search keyword (matches against name, title, description, etc.)
    #[param(min_length = 1, max_length = 200)]
    pub q: String,
    /// Comma-separated list of entity types to search.
    /// Supported: projects, repos, issues, users.
    /// Default: all types.
    pub r#type: Option<String>,
    /// Page number (1-indexed). Default: 1.
    pub page: Option<u32>,
    /// Results per page per type. Default: 20, max: 100.
    pub per_page: Option<u32>,
}

fn parse_types(types: Option<String>) -> Vec<String> {
    match types {
        None => vec![
            "projects".into(),
            "repos".into(),
            "issues".into(),
            "users".into(),
        ],
        Some(s) => {
            let s = s.to_lowercase();
            let mut out = vec![];
            for t in s.split(',') {
                let t = t.trim();
                if t == "projects" || t == "repos" || t == "issues" || t == "users" {
                    out.push(t.into());
                }
            }
            if out.is_empty() {
                vec![
                    "projects".into(),
                    "repos".into(),
                    "issues".into(),
                    "users".into(),
                ]
            } else {
                out
            }
        }
    }
}

fn build_like_pattern(q: &str) -> String {
    format!("%{}%", q.trim())
}

// ─── Per-type result items ───────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ProjectSearchItem {
    pub uid: Uuid,
    pub name: String,
    pub display_name: String,
    pub description: Option<String>,
    pub avatar_url: Option<String>,
    pub is_public: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

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

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct IssueSearchItem {
    pub uid: Uuid,
    pub number: i64,
    pub title: String,
    pub body: Option<String>,
    pub state: String,
    pub project_uid: Uuid,
    pub project_name: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct UserSearchItem {
    pub uid: Uuid,
    pub username: String,
    pub display_name: Option<String>,
    pub avatar_url: Option<String>,
    pub organization: Option<String>,
    pub created_at: DateTime<Utc>,
}

// ─── Per-type result set ─────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct SearchResultSet<T> {
    pub items: Vec<T>,
    pub total: i64,
    pub page: u32,
    pub per_page: u32,
}

impl<T> SearchResultSet<T> {
    fn new(items: Vec<T>, total: i64, page: u32, per_page: u32) -> Self {
        Self {
            items,
            total,
            page,
            per_page,
        }
    }
}

// ─── Aggregated response ──────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct SearchResponse {
    pub query: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub projects: Option<SearchResultSet<ProjectSearchItem>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repos: Option<SearchResultSet<RepoSearchItem>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub issues: Option<SearchResultSet<IssueSearchItem>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub users: Option<SearchResultSet<UserSearchItem>>,
}

// ─── Permission helpers ───────────────────────────────────────────────────────

/// Returns the set of project IDs the current user can access (for filtering).
/// If user is None (anonymous), returns only public project IDs.
async fn accessible_project_ids(
    db: &AppDatabase,
    user_id: Option<Uuid>,
) -> Result<Vec<Uuid>, AppError> {
    let public_projects: Vec<Uuid> = project::Entity::find()
        .filter(project::Column::IsPublic.eq(true))
        .select_only()
        .column(project::Column::Id)
        .into_tuple::<Uuid>()
        .all(db)
        .await
        .map_err(|_| AppError::InternalError)?;

    let Some(user_id) = user_id else {
        // Anonymous: only public projects
        return Ok(public_projects);
    };

    let memberships: Vec<Uuid> = project_members::Entity::find()
        .filter(project_members::Column::User.eq(user_id))
        .select_only()
        .column(project_members::Column::Project)
        .into_tuple::<Uuid>()
        .all(db)
        .await
        .map_err(|_| AppError::InternalError)?;

    let mut all: Vec<Uuid> = public_projects;
    for id in memberships {
        if !all.iter().any(|x| x == &id) {
            all.push(id);
        }
    }
    Ok(all)
}

impl AppService {
    pub async fn search(
        &self,
        ctx: &Session,
        params: SearchQuery,
    ) -> Result<SearchResponse, AppError> {
        let page = Ord::max(params.page.unwrap_or(1), 1u32);
        let per_page = Ord::min(params.per_page.unwrap_or(20), 100u32);
        let query = params.q.trim();
        if query.is_empty() {
            return Err(AppError::BadRequest("q is required".to_string()));
        }

        let types = parse_types(params.r#type);
        let user_id = ctx.user();
        let accessible = accessible_project_ids(&self.db, user_id).await?;

        let mut resp = SearchResponse {
            query: query.to_string(),
            projects: None,
            repos: None,
            issues: None,
            users: None,
        };

        if types.iter().any(|t| t == "projects") {
            match self
                .search_projects(query, &accessible, page, per_page)
                .await
            {
                Ok(set) => resp.projects = Some(set),
                Err(_) => {}
            }
        }
        if types.iter().any(|t| t == "repos") {
            if let Ok(set) = self.search_repos(query, &accessible, page, per_page).await {
                resp.repos = Some(set);
            }
        }
        if types.iter().any(|t| t == "issues") {
            if let Ok(set) = self.search_issues(query, &accessible, page, per_page).await {
                resp.issues = Some(set);
            }
        }
        if types.iter().any(|t| t == "users") {
            if let Ok(users) = self.search_users(query, page, per_page).await {
                resp.users = Some(users);
            }
        }

        Ok(resp)
    }

    async fn search_projects(
        &self,
        query: &str,
        accessible: &[Uuid],
        page: u32,
        per_page: u32,
    ) -> Result<SearchResultSet<ProjectSearchItem>, AppError> {
        let offset = (page - 1) * per_page;
        let pattern = build_like_pattern(query);

        // OR filter: Name ILIKE q OR DisplayName ILIKE q OR Description ILIKE q
        let or_filter = SqExpr::col(project::Column::Name)
            .ilike(&pattern)
            .or(SqExpr::col(project::Column::DisplayName).ilike(&pattern))
            .or(SqExpr::col(project::Column::Description).ilike(&pattern));

        // Count
        let total: i64 = project::Entity::find()
            .filter(project::Column::Id.is_in(accessible.iter().cloned().collect::<Vec<_>>()))
            .filter(or_filter.clone())
            .count(&self.db)
            .await
            .map_err(|_| AppError::InternalError)? as i64;

        // Fetch
        let items: Vec<ProjectSearchItem> = project::Entity::find()
            .filter(project::Column::Id.is_in(accessible.iter().cloned().collect::<Vec<_>>()))
            .filter(or_filter)
            .order_by_desc(project::Column::UpdatedAt)
            .offset(Some(offset as u64))
            .limit(Some(per_page as u64))
            .all(&self.db)
            .await
            .map_err(|_| AppError::InternalError)?
            .into_iter()
            .map(|p| ProjectSearchItem {
                uid: p.id,
                name: p.name,
                display_name: p.display_name,
                description: p.description,
                avatar_url: p.avatar_url,
                is_public: p.is_public,
                created_at: p.created_at,
                updated_at: p.updated_at,
            })
            .collect();

        Ok(SearchResultSet::new(items, total, page, per_page))
    }

    async fn search_repos(
        &self,
        query: &str,
        accessible: &[Uuid],
        page: u32,
        per_page: u32,
    ) -> Result<SearchResultSet<RepoSearchItem>, AppError> {
        let offset = (page - 1) * per_page;
        let pattern = build_like_pattern(query);

        // OR filter: RepoName ILIKE q OR Description ILIKE q
        let repo_or = SqExpr::col(repo::Column::RepoName)
            .ilike(&pattern)
            .or(SqExpr::col(repo::Column::Description).ilike(&pattern));

        // Count
        let total: i64 = repo::Entity::find()
            .filter(repo::Column::Project.is_in(accessible.iter().cloned().collect::<Vec<_>>()))
            .filter(repo_or.clone())
            .count(&self.db)
            .await
            .map_err(|_| AppError::InternalError)? as i64;

        // Fetch
        let repos: Vec<repo::Model> = repo::Entity::find()
            .filter(repo::Column::Project.is_in(accessible.iter().cloned().collect::<Vec<_>>()))
            .filter(repo_or)
            .order_by_desc(repo::Column::UpdatedAt)
            .offset(Some(offset as u64))
            .limit(Some(per_page as u64))
            .all(&self.db)
            .await
            .map_err(|_| AppError::InternalError)?;

        // Batch-fetch project names
        let project_ids: Vec<Uuid> = repos.iter().map(|r| r.project).collect();
        let project_names: std::collections::HashMap<Uuid, String> = project::Entity::find()
            .filter(project::Column::Id.is_in(project_ids.clone()))
            .into_tuple::<(Uuid, String)>()
            .all(&self.db)
            .await
            .map_err(|_| AppError::InternalError)?
            .into_iter()
            .collect();

        let items: Vec<RepoSearchItem> = repos
            .into_iter()
            .map(|r| RepoSearchItem {
                uid: r.id,
                name: r.repo_name,
                description: r.description,
                project_uid: r.project,
                project_name: project_names
                    .get(&r.project)
                    .cloned()
                    .unwrap_or_else(|| r.project.to_string()),
                is_private: r.is_private,
                created_at: r.created_at,
            })
            .collect();

        Ok(SearchResultSet::new(items, total, page, per_page))
    }

    async fn search_issues(
        &self,
        query: &str,
        accessible: &[Uuid],
        page: u32,
        per_page: u32,
    ) -> Result<SearchResultSet<IssueSearchItem>, AppError> {
        let offset = (page - 1) * per_page;
        let pattern = build_like_pattern(query);

        // OR filter: Title ILIKE q OR Body ILIKE q
        let issue_or = SqExpr::col(issue::Column::Title)
            .ilike(&pattern)
            .or(SqExpr::col(issue::Column::Body).ilike(&pattern));

        // Count
        let total: i64 = issue::Entity::find()
            .filter(issue::Column::Project.is_in(accessible.iter().cloned().collect::<Vec<_>>()))
            .filter(issue_or.clone())
            .count(&self.db)
            .await
            .map_err(|_| AppError::InternalError)? as i64;

        // Fetch
        let issues: Vec<issue::Model> = issue::Entity::find()
            .filter(issue::Column::Project.is_in(accessible.iter().cloned().collect::<Vec<_>>()))
            .filter(issue_or)
            .order_by_desc(issue::Column::UpdatedAt)
            .offset(Some(offset as u64))
            .limit(Some(per_page as u64))
            .all(&self.db)
            .await
            .map_err(|_| AppError::InternalError)?;

        // Batch-fetch project names
        let project_ids: Vec<Uuid> = issues.iter().map(|i| i.project).collect();
        let project_names: std::collections::HashMap<Uuid, String> = project::Entity::find()
            .filter(project::Column::Id.is_in(project_ids.clone()))
            .into_tuple::<(Uuid, String)>()
            .all(&self.db)
            .await
            .map_err(|_| AppError::InternalError)?
            .into_iter()
            .collect();

        let items: Vec<IssueSearchItem> = issues
            .into_iter()
            .map(|i| IssueSearchItem {
                uid: i.id,
                number: i.number,
                title: i.title,
                body: i.body,
                state: i.state,
                project_uid: i.project,
                project_name: project_names
                    .get(&i.project)
                    .cloned()
                    .unwrap_or_else(|| i.project.to_string()),
                created_at: i.created_at,
                updated_at: i.updated_at,
            })
            .collect();

        Ok(SearchResultSet::new(items, total, page, per_page))
    }

    async fn search_users(
        &self,
        query: &str,
        page: u32,
        per_page: u32,
    ) -> Result<SearchResultSet<UserSearchItem>, AppError> {
        let offset = (page - 1) * per_page;
        let pattern = build_like_pattern(query);

        // OR filter: Username ILIKE q OR DisplayName ILIKE q
        let user_or = SqExpr::col(user::Column::Username)
            .ilike(&pattern)
            .or(SqExpr::col(user::Column::DisplayName).ilike(&pattern));

        // Count
        let total: i64 = user::Entity::find()
            .filter(user_or.clone())
            .count(&self.db)
            .await
            .map_err(|_| AppError::InternalError)? as i64;

        // Fetch
        let items: Vec<UserSearchItem> = user::Entity::find()
            .filter(user_or)
            .order_by_desc(user::Column::LastSignInAt)
            .offset(Some(offset as u64))
            .limit(Some(per_page as u64))
            .all(&self.db)
            .await
            .map_err(|_| AppError::InternalError)?
            .into_iter()
            .map(|u| UserSearchItem {
                uid: u.uid,
                username: u.username,
                display_name: u.display_name,
                avatar_url: u.avatar_url,
                organization: u.organization,
                created_at: u.created_at,
            })
            .collect();

        Ok(SearchResultSet::new(items, total, page, per_page))
    }
}
