use axum::{
    extract::{Json, Path, Query, State},
    http::StatusCode,
};
use diesel::{dsl::exists, prelude::*, PgConnection};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;

use crate::models::{Document, Folder, NewFolder};
use crate::schema::{documents, folders};
use crate::state::AppState;
use crate::{
    auth::TenantScopedConn,
    error::{AppError, AppResult},
};

use super::documents::{hydrate_documents, DocumentResponse};
use crate::utils::{json::deserialize_patch_field, time::to_iso};

#[derive(Deserialize, ToSchema)]
pub struct CreateFolderRequest {
    pub name: String,
    #[schema(nullable)]
    pub parent_id: Option<Uuid>,
}

#[derive(Deserialize, ToSchema)]
pub struct EnsureFolderPathRequest {
    #[schema(nullable)]
    pub parent_id: Option<Uuid>,
    pub segments: Vec<String>,
}

#[derive(Serialize, ToSchema)]
pub struct FolderResponse {
    pub folder: FolderInfo,
}

#[derive(Serialize, ToSchema)]
pub struct FolderContentsResponse {
    #[schema(nullable)]
    pub folder: Option<FolderInfo>,
    pub subfolders: Vec<FolderInfo>,
    pub documents: Vec<DocumentResponse>,
}

#[derive(Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct FolderContentsQuery {
    #[serde(default = "default_include_documents")]
    #[schema(default = true)]
    pub include_documents: bool,
}

const fn default_include_documents() -> bool {
    true
}

#[derive(Serialize, ToSchema)]
pub struct FolderInfo {
    pub id: Uuid,
    pub name: String,
    #[schema(nullable)]
    pub parent_id: Option<Uuid>,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Default, Deserialize, ToSchema)]
pub struct UpdateFolderRequest {
    #[serde(default, deserialize_with = "deserialize_patch_field")]
    #[schema(nullable, value_type = Option<Uuid>)]
    pub parent_id: Option<Option<Uuid>>,
    #[serde(default, deserialize_with = "deserialize_patch_field")]
    #[schema(nullable)]
    pub name: Option<Option<String>>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn update_folder_request_deserializes_null_parent() {
        let req: UpdateFolderRequest =
            serde_json::from_value(json!({ "parent_id": null })).unwrap();
        assert!(matches!(req.parent_id, Some(None)));
    }

    #[test]
    fn update_folder_request_deserializes_absent_parent() {
        let req: UpdateFolderRequest = serde_json::from_value(json!({})).unwrap();
        assert!(req.parent_id.is_none());
    }

    #[test]
    fn update_folder_request_deserializes_null_name() {
        let req: UpdateFolderRequest = serde_json::from_value(json!({ "name": null })).unwrap();
        assert!(matches!(req.name, Some(None)));
    }
}

#[utoipa::path(
    get,
    path = "/api/folders/{id}",
    params(("id" = Uuid, Path, description = "Folder ID")),
    responses((status = 200, description = "Folder detail", body = FolderResponse)),
    tag = "Folders"
)]
pub async fn get_folder(
    Path(folder_id): Path<Uuid>,
    TenantScopedConn {
        mut conn,
        tenant_id,
        ..
    }: TenantScopedConn,
) -> AppResult<Json<FolderResponse>> {
    let folder: Folder = folders::table
        .find(folder_id)
        .filter(folders::tenant_id.eq(tenant_id))
        .first(&mut conn)?;

    Ok(Json(FolderResponse {
        folder: folder_to_info(folder),
    }))
}

#[utoipa::path(
    post,
    path = "/api/folders/path",
    request_body = EnsureFolderPathRequest,
    responses((status = 200, description = "Folder path ensured", body = FolderResponse)),
    tag = "Folders"
)]
pub async fn ensure_folder_path(
    TenantScopedConn {
        mut conn,
        tenant_id,
        ..
    }: TenantScopedConn,
    Json(payload): Json<EnsureFolderPathRequest>,
) -> AppResult<Json<FolderResponse>> {
    if payload.segments.is_empty() {
        return Err(AppError::bad_request("segments must not be empty"));
    }

    let target_folder = conn.transaction::<Folder, AppError, _>(|conn| {
        let mut current_parent = payload.parent_id;
        let mut last_folder: Option<Folder> = None;

        for raw_name in &payload.segments {
            let name = raw_name.trim();
            if name.is_empty() {
                return Err(AppError::bad_request("folder names must not be empty"));
            }

            let existing: Option<Folder> = if let Some(parent_id) = current_parent {
                folders::table
                    .filter(folders::tenant_id.eq(tenant_id))
                    .filter(folders::parent_id.eq(Some(parent_id)))
                    .filter(folders::name.eq(name))
                    .first(conn)
                    .optional()?
            } else {
                folders::table
                    .filter(folders::tenant_id.eq(tenant_id))
                    .filter(folders::parent_id.is_null())
                    .filter(folders::name.eq(name))
                    .first(conn)
                    .optional()?
            };

            let folder = if let Some(folder) = existing {
                folder
            } else {
                let new_folder = NewFolder {
                    id: Uuid::new_v4(),
                    name: name.to_string(),
                    parent_id: current_parent,
                    tenant_id,
                };

                let inserted_id: Option<Uuid> = diesel::insert_into(folders::table)
                    .values(&new_folder)
                    .on_conflict_do_nothing()
                    .returning(folders::id)
                    .get_result(conn)
                    .optional()?;

                if let Some(id) = inserted_id {
                    folders::table
                        .find(id)
                        .filter(folders::tenant_id.eq(tenant_id))
                        .first(conn)?
                } else if let Some(parent_id) = current_parent {
                    folders::table
                        .filter(folders::tenant_id.eq(tenant_id))
                        .filter(folders::parent_id.eq(Some(parent_id)))
                        .filter(folders::name.eq(name))
                        .first(conn)?
                } else {
                    folders::table
                        .filter(folders::tenant_id.eq(tenant_id))
                        .filter(folders::parent_id.is_null())
                        .filter(folders::name.eq(name))
                        .first(conn)?
                }
            };
            current_parent = Some(folder.id);
            last_folder = Some(folder);
        }

        last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path"))
    })?;

    Ok(Json(FolderResponse {
        folder: folder_to_info(target_folder),
    }))
}

#[utoipa::path(
    post,
    path = "/api/folders",
    request_body = CreateFolderRequest,
    responses(
        (status = 201, description = "Folder created", body = FolderResponse),
        (status = 200, description = "Folder already existed", body = FolderResponse)
    ),
    tag = "Folders"
)]
pub async fn create_folder(
    TenantScopedConn {
        mut conn,
        tenant_id,
        ..
    }: TenantScopedConn,
    Json(payload): Json<CreateFolderRequest>,
) -> AppResult<(StatusCode, Json<FolderResponse>)> {
    if payload.name.trim().is_empty() {
        return Err(AppError::bad_request("name must not be empty"));
    }

    let name = payload.name.trim();

    let existing: Option<Folder> = if let Some(parent_id) = payload.parent_id {
        folders::table
            .filter(folders::tenant_id.eq(tenant_id))
            .filter(folders::parent_id.eq(Some(parent_id)))
            .filter(folders::name.eq(name))
            .first(&mut conn)
            .optional()?
    } else {
        folders::table
            .filter(folders::tenant_id.eq(tenant_id))
            .filter(folders::parent_id.is_null())
            .filter(folders::name.eq(name))
            .first(&mut conn)
            .optional()?
    };

    let (folder, created): (Folder, bool) = if let Some(folder) = existing {
        (folder, false)
    } else {
        let new_folder = NewFolder {
            id: Uuid::new_v4(),
            name: name.to_string(),
            parent_id: payload.parent_id,
            tenant_id,
        };

        let inserted_id: Option<Uuid> = diesel::insert_into(folders::table)
            .values(&new_folder)
            .on_conflict_do_nothing()
            .returning(folders::id)
            .get_result(&mut conn)
            .optional()?;

        if let Some(id) = inserted_id {
            (
                folders::table
                    .find(id)
                    .filter(folders::tenant_id.eq(tenant_id))
                    .first(&mut conn)?,
                true,
            )
        } else if let Some(parent_id) = payload.parent_id {
            (
                folders::table
                    .filter(folders::tenant_id.eq(tenant_id))
                    .filter(folders::parent_id.eq(Some(parent_id)))
                    .filter(folders::name.eq(name))
                    .first(&mut conn)?,
                false,
            )
        } else {
            (
                folders::table
                    .filter(folders::tenant_id.eq(tenant_id))
                    .filter(folders::parent_id.is_null())
                    .filter(folders::name.eq(name))
                    .first(&mut conn)?,
                false,
            )
        }
    };

    let response = Json(FolderResponse {
        folder: folder_to_info(folder),
    });

    if created {
        Ok((StatusCode::CREATED, response))
    } else {
        Ok((StatusCode::OK, response))
    }
}

#[utoipa::path(
    get,
    path = "/api/folders/{id}/contents",
    params(("id" = Uuid, Path, description = "Folder ID"), FolderContentsQuery),
    responses((status = 200, description = "Folder contents", body = FolderContentsResponse)),
    tag = "Folders"
)]
pub async fn list_folder_contents(
    State(state): State<AppState>,
    Path(folder_identifier): Path<String>,
    Query(query): Query<FolderContentsQuery>,
    TenantScopedConn {
        mut conn,
        tenant_id,
        user_id,
        ..
    }: TenantScopedConn,
) -> AppResult<Json<FolderContentsResponse>> {
    let folder_id = if folder_identifier.eq_ignore_ascii_case("root") {
        None
    } else {
        Some(
            Uuid::parse_str(&folder_identifier)
                .map_err(|_| AppError::bad_request("folder identifier must be 'root' or a UUID"))?,
        )
    };

    let folder = match folder_id {
        Some(id) => Some(folder_to_info(
            folders::table
                .find(id)
                .filter(folders::tenant_id.eq(tenant_id))
                .first::<Folder>(&mut conn)?,
        )),
        None => None,
    };

    let child_folders: Vec<Folder> = if let Some(parent_id) = folder_id {
        folders::table
            .filter(folders::parent_id.eq(parent_id))
            .filter(folders::tenant_id.eq(tenant_id))
            .order(folders::name.asc())
            .load(&mut conn)?
    } else {
        folders::table
            .filter(folders::parent_id.is_null())
            .filter(folders::tenant_id.eq(tenant_id))
            .order(folders::name.asc())
            .load(&mut conn)?
    };
    let subfolders = child_folders.into_iter().map(folder_to_info).collect();

    let documents = if query.include_documents {
        let docs_query = documents::table
            .filter(documents::deleted_at.is_null())
            .filter(documents::tenant_id.eq(tenant_id))
            .order(documents::created_at.desc());

        let docs: Vec<Document> = if let Some(current_folder) = folder_id {
            docs_query
                .filter(documents::folder_id.eq(current_folder))
                .load(&mut conn)?
        } else {
            docs_query
                .filter(documents::folder_id.is_null())
                .load(&mut conn)?
        };

        hydrate_documents(&state, &mut conn, tenant_id, user_id, docs)?
    } else {
        Vec::new()
    };

    Ok(Json(FolderContentsResponse {
        folder,
        subfolders,
        documents,
    }))
}

#[utoipa::path(
    delete,
    path = "/api/folders/{id}",
    params(("id" = Uuid, Path, description = "Folder ID")),
    responses((status = 204, description = "Folder deleted")),
    tag = "Folders"
)]
pub async fn delete_folder(
    Path(folder_id): Path<Uuid>,
    TenantScopedConn {
        mut conn,
        tenant_id,
        ..
    }: TenantScopedConn,
) -> AppResult<StatusCode> {
    conn.transaction::<_, AppError, _>(|conn| {
        folders::table
            .find(folder_id)
            .filter(folders::tenant_id.eq(tenant_id))
            .first::<Folder>(conn)?;

        let has_child_folders: bool = diesel::select(exists(
            folders::table
                .filter(folders::parent_id.eq(Some(folder_id)))
                .filter(folders::tenant_id.eq(tenant_id)),
        ))
        .get_result(conn)?;

        if has_child_folders {
            return Err(AppError::bad_request(
                "folder must be empty before deletion",
            ));
        }

        let has_documents: bool = diesel::select(exists(
            documents::table
                .filter(documents::folder_id.eq(Some(folder_id)))
                .filter(documents::tenant_id.eq(tenant_id))
                .filter(documents::deleted_at.is_null()),
        ))
        .get_result(conn)?;

        if has_documents {
            return Err(AppError::bad_request(
                "folder must be empty before deletion",
            ));
        }

        diesel::delete(
            folders::table
                .filter(folders::id.eq(folder_id))
                .filter(folders::tenant_id.eq(tenant_id)),
        )
        .execute(conn)?;

        Ok(())
    })?;

    Ok(StatusCode::NO_CONTENT)
}

#[utoipa::path(
    patch,
    path = "/api/folders/{id}",
    params(("id" = Uuid, Path, description = "Folder ID")),
    request_body = UpdateFolderRequest,
    responses((status = 204, description = "Folder updated")),
    tag = "Folders"
)]
pub async fn update_folder(
    Path(folder_id): Path<Uuid>,
    TenantScopedConn {
        mut conn,
        tenant_id,
        ..
    }: TenantScopedConn,
    Json(payload): Json<UpdateFolderRequest>,
) -> AppResult<StatusCode> {
    conn.transaction::<(), AppError, _>(|conn| {
        let folder: Folder = folders::table
            .find(folder_id)
            .filter(folders::tenant_id.eq(tenant_id))
            .first(conn)?;

        let mut next_parent = folder.parent_id;
        let mut parent_changed = false;
        match payload.parent_id {
            None => {}
            Some(None) => {
                if folder.parent_id.is_some() {
                    parent_changed = true;
                }
                next_parent = None;
            }
            Some(Some(parent_id)) => {
                if parent_id == folder_id {
                    return Err(AppError::bad_request("folder cannot be its own parent"));
                }

                let _parent: Folder = folders::table
                    .find(parent_id)
                    .filter(folders::tenant_id.eq(tenant_id))
                    .first(conn)?;

                if folder.parent_id != Some(parent_id) {
                    let descendant_ids = gather_descendant_folder_ids(conn, tenant_id, folder_id)?;
                    if descendant_ids.contains(&parent_id) {
                        return Err(AppError::bad_request(
                            "cannot move folder into itself or a descendant",
                        ));
                    }
                    parent_changed = true;
                }

                next_parent = Some(parent_id);
            }
        }

        let mut new_name = folder.name.clone();
        let mut name_changed = false;
        match payload.name {
            None => {}
            Some(None) => {
                return Err(AppError::bad_request("name cannot be null"));
            }
            Some(Some(value)) => {
                let trimmed = value.trim();
                if trimmed.is_empty() {
                    return Err(AppError::bad_request("name must not be empty"));
                }

                if trimmed != folder.name {
                    new_name = trimmed.to_string();
                    name_changed = true;
                }
            }
        }

        if !parent_changed && !name_changed {
            return Ok(());
        }

        let conflict = if let Some(parent_id) = next_parent {
            folders::table
                .filter(folders::parent_id.eq(Some(parent_id)))
                .filter(folders::name.eq(&new_name))
                .filter(folders::id.ne(folder_id))
                .filter(folders::tenant_id.eq(tenant_id))
                .first::<Folder>(conn)
                .optional()?
        } else {
            folders::table
                .filter(folders::parent_id.is_null())
                .filter(folders::name.eq(&new_name))
                .filter(folders::id.ne(folder_id))
                .filter(folders::tenant_id.eq(tenant_id))
                .first::<Folder>(conn)
                .optional()?
        };

        if conflict.is_some() {
            return Err(AppError::bad_request(
                "a folder with the same name already exists in the target",
            ));
        }

        diesel::update(
            folders::table
                .find(folder_id)
                .filter(folders::tenant_id.eq(tenant_id)),
        )
        .set((
            folders::parent_id.eq(next_parent),
            folders::name.eq(&new_name),
        ))
        .execute(conn)?;

        Ok(())
    })?;

    Ok(StatusCode::NO_CONTENT)
}

fn folder_to_info(folder: Folder) -> FolderInfo {
    FolderInfo {
        id: folder.id,
        name: folder.name,
        parent_id: folder.parent_id,
        created_at: to_iso(folder.created_at),
        updated_at: to_iso(folder.updated_at),
    }
}

pub(super) fn gather_descendant_folder_ids(
    conn: &mut PgConnection,
    tenant_id: Uuid,
    folder_id: Uuid,
) -> AppResult<Vec<Uuid>> {
    let mut ids = vec![folder_id];
    let mut queue = vec![folder_id];

    while let Some(current) = queue.pop() {
        let child_ids: Vec<Uuid> = folders::table
            .filter(folders::parent_id.eq(Some(current)))
            .filter(folders::tenant_id.eq(tenant_id))
            .select(folders::id)
            .load(conn)?;
        queue.extend(child_ids.iter().copied());
        ids.extend(child_ids);
    }

    Ok(ids)
}

#[derive(utoipa::OpenApi)]
#[openapi(
    paths(
        crate::routes::folders::create_folder,
        crate::routes::folders::ensure_folder_path,
        crate::routes::folders::get_folder,
        crate::routes::folders::list_folder_contents,
        crate::routes::folders::delete_folder,
        crate::routes::folders::update_folder
    ),
    components(schemas(
        crate::routes::folders::CreateFolderRequest,
        crate::routes::folders::EnsureFolderPathRequest,
        crate::routes::folders::FolderResponse,
        crate::routes::folders::FolderInfo,
        crate::routes::folders::FolderContentsQuery,
        crate::routes::folders::FolderContentsResponse,
        crate::routes::folders::UpdateFolderRequest
    ))
)]
pub struct FoldersApiDoc;
