use crate::auth::checks::is_visible_collection;
use crate::auth::{filter_visible_collections, get_user_from_headers};
use crate::database::PgPool;
use crate::database::models::{
    collection_item, generate_collection_id, project_item,
};
use crate::database::redis::RedisPool;
use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::collections::{Collection, CollectionStatus};
use crate::models::ids::{CollectionId, ProjectId};
use crate::models::pats::Scopes;
use crate::models::v3::user_limits::UserLimits;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::routes::v3::project_creation::CreateError;
use crate::util::error::Context;
use crate::util::img::delete_old_images;
use crate::util::routes::read_limited_from_payload;
use crate::util::validate::validation_errors_to_string;
use crate::{database, models};
use actix_web::web::Data;
use actix_web::{HttpRequest, HttpResponse, web};
use ariadne::ids::base62_impl::parse_base62;
use chrono::Utc;
use eyre::eyre;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use validator::Validate;

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.route("collections", web::get().to(collections_get));
    cfg.route("collection", web::post().to(collection_create));

    cfg.service(
        web::scope("collection")
            .route("{id}", web::get().to(collection_get))
            .route("{id}", web::delete().to(collection_delete))
            .route("{id}", web::patch().to(collection_edit))
            .route("{id}/icon", web::patch().to(collection_icon_edit))
            .route("{id}/icon", web::delete().to(delete_collection_icon)),
    );
}

#[derive(Serialize, Deserialize, Validate, Clone)]
pub struct CollectionCreateData {
    #[validate(
        length(min = 3, max = 64),
        custom(function = "crate::util::validate::validate_name")
    )]
    /// The title or name of the project.
    pub name: String,
    #[validate(length(min = 3, max = 255))]
    /// A short description of the collection.
    pub description: Option<String>,
    #[validate(length(max = 1024))]
    #[serde(default = "Vec::new")]
    /// A list of initial projects to use with the created collection
    pub projects: Vec<String>,
}

pub async fn collection_create(
    req: HttpRequest,
    collection_create_data: web::Json<CollectionCreateData>,
    client: Data<PgPool>,
    redis: Data<RedisPool>,
    session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, CreateError> {
    let collection_create_data = collection_create_data.into_inner();

    // The currently logged in user
    let current_user = get_user_from_headers(
        &req,
        &**client,
        &redis,
        &session_queue,
        Scopes::COLLECTION_CREATE,
    )
    .await?
    .1;

    let limits =
        UserLimits::get_for_collections(&current_user, &client).await?;
    if limits.current >= limits.max {
        return Err(CreateError::LimitReached);
    }

    collection_create_data.validate().map_err(|err| {
        CreateError::InvalidInput(validation_errors_to_string(err, None))
    })?;

    let mut transaction = client.begin().await?;

    let collection_id: CollectionId =
        generate_collection_id(&mut transaction).await?.into();

    let initial_project_ids = project_item::DBProject::get_many(
        &collection_create_data.projects,
        &mut transaction,
        &redis,
    )
    .await
    .wrap_internal_err("failed to fetch created projects")?
    .into_iter()
    .map(|x| x.inner.id.into())
    .collect::<Vec<ProjectId>>();

    let collection_builder_actual = collection_item::CollectionBuilder {
        collection_id: collection_id.into(),
        user_id: current_user.id.into(),
        name: collection_create_data.name,
        description: collection_create_data.description,
        status: CollectionStatus::Listed,
        projects: initial_project_ids
            .iter()
            .copied()
            .map(|x| x.into())
            .collect(),
    };
    let collection_builder = collection_builder_actual.clone();

    let now = Utc::now();
    collection_builder_actual.insert(&mut transaction).await?;

    let response = crate::models::collections::Collection {
        id: collection_id,
        user: collection_builder.user_id.into(),
        name: collection_builder.name.clone(),
        description: collection_builder.description.clone(),
        created: now,
        updated: now,
        icon_url: None,
        color: None,
        status: collection_builder.status,
        projects: initial_project_ids,
    };
    transaction.commit().await?;

    Ok(HttpResponse::Ok().json(response))
}

#[derive(Serialize, Deserialize)]
pub struct CollectionIds {
    pub ids: String,
}
pub async fn collections_get(
    req: HttpRequest,
    web::Query(ids): web::Query<CollectionIds>,
    pool: web::Data<PgPool>,
    redis: web::Data<RedisPool>,
    session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
    let ids = serde_json::from_str::<Vec<&str>>(&ids.ids)?;
    let ids = ids
        .into_iter()
        .map(|x| {
            parse_base62(x).map(|x| database::models::DBCollectionId(x as i64))
        })
        .collect::<Result<Vec<_>, _>>()?;

    let collections_data =
        database::models::DBCollection::get_many(&ids, &**pool, &redis).await?;

    let user_option = get_user_from_headers(
        &req,
        &**pool,
        &redis,
        &session_queue,
        Scopes::COLLECTION_READ,
    )
    .await
    .map(|x| x.1)
    .ok();

    let collections =
        filter_visible_collections(collections_data, &user_option, false)
            .await?;

    Ok(HttpResponse::Ok().json(collections))
}

pub async fn collection_get(
    req: HttpRequest,
    info: web::Path<(String,)>,
    pool: web::Data<PgPool>,
    redis: web::Data<RedisPool>,
    session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
    let string = info.into_inner().0;

    let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
    let collection_data =
        database::models::DBCollection::get(id, &**pool, &redis).await?;
    let user_option = get_user_from_headers(
        &req,
        &**pool,
        &redis,
        &session_queue,
        Scopes::COLLECTION_READ,
    )
    .await
    .map(|x| x.1)
    .ok();

    if let Some(data) = collection_data
        && is_visible_collection(&data, &user_option, false).await?
    {
        return Ok(HttpResponse::Ok().json(Collection::from(data)));
    }
    Err(ApiError::NotFound)
}

#[derive(Deserialize, Validate)]
pub struct EditCollection {
    #[validate(
        length(min = 3, max = 64),
        custom(function = "crate::util::validate::validate_name")
    )]
    pub name: Option<String>,
    #[validate(length(min = 3, max = 256))]
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "::serde_with::rust::double_option"
    )]
    pub description: Option<Option<String>>,
    pub status: Option<CollectionStatus>,
    #[validate(length(max = 1024))]
    pub new_projects: Option<Vec<String>>,
}

pub async fn collection_edit(
    req: HttpRequest,
    info: web::Path<(String,)>,
    pool: web::Data<PgPool>,
    new_collection: web::Json<EditCollection>,
    redis: web::Data<RedisPool>,
    session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
    let user = get_user_from_headers(
        &req,
        &**pool,
        &redis,
        &session_queue,
        Scopes::COLLECTION_WRITE,
    )
    .await?
    .1;

    new_collection.validate().map_err(|err| {
        ApiError::Validation(validation_errors_to_string(err, None))
    })?;

    let string = info.into_inner().0;
    let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
    let result =
        database::models::DBCollection::get(id, &**pool, &redis).await?;

    if let Some(collection_item) = result {
        if !can_modify_collection(&collection_item, &user) {
            return Ok(HttpResponse::Unauthorized().body(""));
        }

        let id = collection_item.id;

        let mut transaction = pool.begin().await?;

        if let Some(name) = &new_collection.name {
            sqlx::query!(
                "
                UPDATE collections
                SET name = $1
                WHERE (id = $2)
                ",
                name.trim(),
                id as database::models::ids::DBCollectionId,
            )
            .execute(&mut transaction)
            .await?;
        }

        if let Some(description) = &new_collection.description {
            sqlx::query!(
                "
                UPDATE collections
                SET description = $1
                WHERE (id = $2)
                ",
                description.as_ref(),
                id as database::models::ids::DBCollectionId,
            )
            .execute(&mut transaction)
            .await?;
        }

        if let Some(status) = &new_collection.status {
            if !(user.role.is_mod()
                || collection_item.status.is_approved()
                    && status.can_be_requested())
            {
                return Err(ApiError::CustomAuthentication(
                    "You don't have permission to set this status!".to_string(),
                ));
            }

            sqlx::query!(
                "
                UPDATE collections
                SET status = $1
                WHERE (id = $2)
                ",
                status.to_string(),
                id as database::models::ids::DBCollectionId,
            )
            .execute(&mut transaction)
            .await?;
        }

        if let Some(new_project_ids) = &new_collection.new_projects {
            // Delete all existing projects
            sqlx::query!(
                "
                DELETE FROM collections_mods
                WHERE collection_id = $1
                ",
                collection_item.id as database::models::ids::DBCollectionId,
            )
            .execute(&mut transaction)
            .await?;

            let collection_item_ids = new_project_ids
                .iter()
                .map(|_| collection_item.id.0)
                .collect_vec();
            let mut validated_project_ids = Vec::new();
            for project_id in new_project_ids {
                let project = database::models::DBProject::get(
                    project_id, &**pool, &redis,
                )
                .await?
                .wrap_request_err_with(|| {
                    eyre!("The specified project {project_id} does not exist!")
                })?;
                validated_project_ids.push(project.inner.id.0);
            }
            // Insert- don't throw an error if it already exists
            sqlx::query!(
                "
                        INSERT INTO collections_mods (collection_id, mod_id)
                        SELECT * FROM UNNEST ($1::int8[], $2::int8[])
                        ON CONFLICT DO NOTHING
                        ",
                &collection_item_ids[..],
                &validated_project_ids[..],
            )
            .execute(&mut transaction)
            .await?;

            sqlx::query!(
                "
                UPDATE collections
                SET updated = NOW()
                WHERE id = $1
                ",
                collection_item.id as database::models::ids::DBCollectionId,
            )
            .execute(&mut transaction)
            .await?;
        }

        transaction.commit().await?;
        database::models::DBCollection::clear_cache(collection_item.id, &redis)
            .await?;

        Ok(HttpResponse::NoContent().body(""))
    } else {
        Err(ApiError::NotFound)
    }
}

#[derive(Serialize, Deserialize)]
pub struct Extension {
    pub ext: String,
}

#[allow(clippy::too_many_arguments)]
pub async fn collection_icon_edit(
    web::Query(ext): web::Query<Extension>,
    req: HttpRequest,
    info: web::Path<(String,)>,
    pool: web::Data<PgPool>,
    redis: web::Data<RedisPool>,
    file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
    mut payload: web::Payload,
    session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
    let user = get_user_from_headers(
        &req,
        &**pool,
        &redis,
        &session_queue,
        Scopes::COLLECTION_WRITE,
    )
    .await?
    .1;

    let string = info.into_inner().0;
    let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
    let collection_item =
        database::models::DBCollection::get(id, &**pool, &redis)
            .await?
            .ok_or_else(|| {
                ApiError::InvalidInput(
                    "The specified collection does not exist!".to_string(),
                )
            })?;

    if !can_modify_collection(&collection_item, &user) {
        return Ok(HttpResponse::Unauthorized().body(""));
    }

    delete_old_images(
        collection_item.icon_url,
        collection_item.raw_icon_url,
        FileHostPublicity::Public,
        &***file_host,
    )
    .await?;

    let bytes = read_limited_from_payload(
        &mut payload,
        262144,
        "Icons must be smaller than 256KiB",
    )
    .await?;

    let collection_id: CollectionId = collection_item.id.into();
    let upload_result = crate::util::img::upload_image_optimized(
        &format!("data/{collection_id}"),
        FileHostPublicity::Public,
        bytes.freeze(),
        &ext.ext,
        Some(96),
        Some(1.0),
        &***file_host,
    )
    .await?;

    let mut transaction = pool.begin().await?;

    sqlx::query!(
        "
        UPDATE collections
        SET icon_url = $1, raw_icon_url = $2, color = $3
        WHERE (id = $4)
        ",
        upload_result.url,
        upload_result.raw_url,
        upload_result.color.map(|x| x as i32),
        collection_item.id as database::models::ids::DBCollectionId,
    )
    .execute(&mut transaction)
    .await?;

    transaction.commit().await?;
    database::models::DBCollection::clear_cache(collection_item.id, &redis)
        .await?;

    Ok(HttpResponse::NoContent().body(""))
}

pub async fn delete_collection_icon(
    req: HttpRequest,
    info: web::Path<(String,)>,
    pool: web::Data<PgPool>,
    redis: web::Data<RedisPool>,
    file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
    session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
    let user = get_user_from_headers(
        &req,
        &**pool,
        &redis,
        &session_queue,
        Scopes::COLLECTION_WRITE,
    )
    .await?
    .1;

    let string = info.into_inner().0;
    let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
    let collection_item =
        database::models::DBCollection::get(id, &**pool, &redis)
            .await?
            .ok_or_else(|| {
                ApiError::InvalidInput(
                    "The specified collection does not exist!".to_string(),
                )
            })?;
    if !can_modify_collection(&collection_item, &user) {
        return Ok(HttpResponse::Unauthorized().body(""));
    }

    delete_old_images(
        collection_item.icon_url,
        collection_item.raw_icon_url,
        FileHostPublicity::Public,
        &***file_host,
    )
    .await?;
    let mut transaction = pool.begin().await?;

    sqlx::query!(
        "
        UPDATE collections
        SET icon_url = NULL, raw_icon_url = NULL, color = NULL
        WHERE (id = $1)
        ",
        collection_item.id as database::models::ids::DBCollectionId,
    )
    .execute(&mut transaction)
    .await?;

    transaction.commit().await?;
    database::models::DBCollection::clear_cache(collection_item.id, &redis)
        .await?;

    Ok(HttpResponse::NoContent().body(""))
}

pub async fn collection_delete(
    req: HttpRequest,
    info: web::Path<(String,)>,
    pool: web::Data<PgPool>,
    redis: web::Data<RedisPool>,
    session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
    let user = get_user_from_headers(
        &req,
        &**pool,
        &redis,
        &session_queue,
        Scopes::COLLECTION_DELETE,
    )
    .await?
    .1;

    let string = info.into_inner().0;
    let id = database::models::DBCollectionId(parse_base62(&string)? as i64);
    let collection = database::models::DBCollection::get(id, &**pool, &redis)
        .await?
        .ok_or_else(|| {
            ApiError::InvalidInput(
                "The specified collection does not exist!".to_string(),
            )
        })?;
    if !can_modify_collection(&collection, &user) {
        return Ok(HttpResponse::Unauthorized().body(""));
    }
    let mut transaction = pool.begin().await?;

    let result = database::models::DBCollection::remove(
        collection.id,
        &mut transaction,
        &redis,
    )
    .await?;

    transaction.commit().await?;
    database::models::DBCollection::clear_cache(collection.id, &redis).await?;

    if result.is_some() {
        Ok(HttpResponse::NoContent().body(""))
    } else {
        Err(ApiError::NotFound)
    }
}

fn can_modify_collection(
    collection: &database::models::DBCollection,
    user: &models::users::User,
) -> bool {
    collection.user_id == user.id.into() || user.role.is_mod()
}
