use actix_web::{HttpResponse, web};
use serde::Serialize;
use utoipa::ToSchema;

use crate::channel::ChannelBus;
use crate::error::ApiError;
use service::AppService;
use session::Session;

#[derive(Debug, serde::Deserialize, utoipa::IntoParams)]
pub struct SearchQuery {
    pub q: Option<String>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct SearchResponse {
    pub workspaces: SearchGroup<WorkspaceHit>,
    pub repos: SearchGroup<RepoHit>,
    pub rooms: SearchGroup<RoomHit>,
    pub issues: SearchGroup<IssueHit>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct SearchGroup<T: Serialize> {
    pub items: Vec<T>,
    pub total: usize,
    pub has_more: bool,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct WorkspaceHit {
    pub name: String,
    pub description: Option<String>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct RepoHit {
    pub name: String,
    pub workspace: String,
    pub description: Option<String>,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct RoomHit {
    pub id: String,
    pub name: String,
    pub workspace: String,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct IssueHit {
    pub number: i32,
    pub title: String,
    pub state: String,
    pub workspace: String,
}

const MAX_PER_GROUP: usize = 10;

fn session_user(session: &Session) -> Result<uuid::Uuid, ApiError> {
    session
        .user()
        .ok_or(ApiError(service::error::AppError::Unauthorized))
}

#[utoipa::path(
    get,
    path = "/api/v1/search",
    params(SearchQuery),
    responses((status = 200, body = SearchResponse)),
    security(("session" = [])),
    tag = "search",
)]
pub async fn search(
    session: Session,
    service: web::Data<AppService>,
    bus: web::Data<ChannelBus>,
    query: web::Query<SearchQuery>,
) -> Result<HttpResponse, ApiError> {
    let user_id = session_user(&session)?;
    let q = query.q.as_deref().unwrap_or("").trim().to_lowercase();

    let workspaces = search_workspaces(&service, user_id, &q).await?;
    let repos = search_repos(&service, user_id, &q).await?;
    let rooms = search_rooms(&bus, user_id, &q).await?;
    let issues = search_issues(&service, user_id, &q).await?;

    Ok(HttpResponse::Ok().json(SearchResponse {
        workspaces,
        repos,
        rooms,
        issues,
    }))
}

async fn search_workspaces(
    service: &AppService,
    user_id: uuid::Uuid,
    q: &str,
) -> Result<SearchGroup<WorkspaceHit>, ApiError> {
    let all: Vec<WorkspaceHit> = service
        .workspace_my_inner(user_id)
        .await?
        .into_iter()
        .map(|(name, desc)| WorkspaceHit {
            name,
            description: desc,
        })
        .collect();

    let filtered: Vec<WorkspaceHit> = if q.is_empty() {
        all
    } else {
        all.into_iter()
            .filter(|w| {
                w.name.to_lowercase().contains(q)
                    || w.description
                        .as_deref()
                        .unwrap_or("")
                        .to_lowercase()
                        .contains(q)
            })
            .collect()
    };

    Ok(group_result(filtered))
}

async fn search_repos(
    service: &AppService,
    user_id: uuid::Uuid,
    q: &str,
) -> Result<SearchGroup<RepoHit>, ApiError> {
    let workspaces: Vec<String> = service
        .workspace_my_inner(user_id)
        .await?
        .into_iter()
        .map(|(name, _)| name)
        .collect();

    let mut all: Vec<RepoHit> = Vec::new();
    for wk in &workspaces {
        if let Ok(repos) = service.repo_list_inner(wk).await {
            for (name, desc) in repos {
                all.push(RepoHit {
                    name,
                    workspace: wk.clone(),
                    description: desc,
                });
            }
        }
    }

    let filtered: Vec<RepoHit> = if q.is_empty() {
        all
    } else {
        let q = q;
        all.into_iter()
            .filter(|r| {
                format!(
                    "{} {} {}",
                    r.workspace,
                    r.name,
                    r.description.as_deref().unwrap_or("")
                )
                .to_lowercase()
                .contains(q)
            })
            .collect()
    };

    Ok(group_result(filtered))
}

async fn search_rooms(
    bus: &ChannelBus,
    user_id: uuid::Uuid,
    q: &str,
) -> Result<SearchGroup<RoomHit>, ApiError> {
    let rooms = bus.list_user_rooms(user_id).await.map_err(|e| {
        ApiError(service::error::AppError::InternalServerError(e.to_string()))
    })?;

    let all: Vec<RoomHit> = rooms
        .into_iter()
        .map(|r| RoomHit {
            id: r.id.to_string(),
            name: r.name,
            workspace: r.workspace_id.to_string(),
        })
        .collect();

    let filtered: Vec<RoomHit> = if q.is_empty() {
        all
    } else {
        all.into_iter()
            .filter(|r| r.name.to_lowercase().contains(q))
            .collect()
    };

    Ok(group_result(filtered))
}

async fn search_issues(
    service: &AppService,
    user_id: uuid::Uuid,
    q: &str,
) -> Result<SearchGroup<IssueHit>, ApiError> {
    let workspaces: Vec<String> = service
        .workspace_my_inner(user_id)
        .await?
        .into_iter()
        .map(|(name, _)| name)
        .collect();

    let mut all: Vec<IssueHit> = Vec::new();
    for wk in &workspaces {
        if let Ok(issues) = service.issue_list_inner(wk).await {
            for (number, title, state) in issues {
                all.push(IssueHit {
                    number,
                    title,
                    state,
                    workspace: wk.clone(),
                });
            }
        }
    }

    let filtered: Vec<IssueHit> = if q.is_empty() {
        all
    } else {
        all.into_iter()
            .filter(|i| {
                format!("#{} {}", i.number, i.title)
                    .to_lowercase()
                    .contains(q)
            })
            .collect()
    };

    Ok(group_result(filtered))
}

fn group_result<T: Serialize>(mut items: Vec<T>) -> SearchGroup<T> {
    let total = items.len();
    let has_more = total > MAX_PER_GROUP;
    items.truncate(MAX_PER_GROUP);
    SearchGroup {
        items,
        total,
        has_more,
    }
}
