use std::collections::HashSet;

use anyhow::Context as _;
use askama::Template;
use axum::{
    extract::{Path, State},
    response::{Html, IntoResponse, Redirect},
    routing::{get, post},
    Router,
};
use axum_extra::extract::Form;
use diesel::prelude::*;
use serde::Deserialize;
use uuid::Uuid;

use crate::{
    api_keys::ApiKey,
    app_error::AppError,
    app_state::{AppState, DbConn},
    channel_selections::ChannelSelection,
    channels::Channel,
    csrf::generate_csrf_token,
    guards,
    nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_PROJECTS},
    projects::Project,
    schema::channel_selections,
    settings::Settings,
    users::CurrentUser,
};

pub fn new_router() -> Router<AppState> {
    Router::new()
        .route("/teams/{team_id}/projects", get(projects_page))
        .route("/teams/{team_id}/projects/{project_id}", get(project_page))
        .route(
            "/teams/{team_id}/projects/{project_id}/update-enabled-channels",
            post(update_enabled_channels),
        )
}

async fn projects_page(
    State(Settings { base_path, .. }): State<Settings>,
    State(navbar_template): State<NavbarBuilder>,
    DbConn(db_conn): DbConn,
    Path(team_id): Path<Uuid>,
    CurrentUser(current_user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
    let team = guards::require_team_membership(&current_user, &team_id, &db_conn).await?;

    let (api_keys, projects) = {
        let team = team.clone();
        db_conn
            .interact(move |conn| {
                diesel::QueryResult::Ok((
                    team.api_keys().load(conn)?,
                    Project::all()
                        .filter(Project::with_team(&team.id))
                        .load(conn)?,
                ))
            })
            .await
            .unwrap()?
    };

    mod filters {
        use uuid::Uuid;

        pub fn compact_uuid(id: &Uuid) -> askama::Result<String> {
            Ok(crate::api_keys::compact_uuid(id))
        }

        pub fn redact(value: &str) -> askama::Result<String> {
            Ok(format!(
                "********{}",
                &value[value.char_indices().nth_back(3).unwrap().0..]
            ))
        }
    }
    #[derive(Template)]
    #[template(path = "projects.html")]
    struct ResponseTemplate {
        base_path: String,
        breadcrumbs: BreadcrumbTrail,
        csrf_token: String,
        keys: Vec<ApiKey>,
        navbar: Navbar,
        projects: Vec<Project>,
    }
    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
    Ok(Html(
        ResponseTemplate {
            breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
                .with_i18n_slug("en")
                .push_slug("Teams", "teams")
                .push_slug(&team.name, &team.id.simple().to_string())
                .push_slug("Projects", "projects"),
            base_path,
            csrf_token,
            navbar: navbar_template
                .with_param("team_id", &team.id.simple().to_string())
                .with_active_item(NAVBAR_ITEM_PROJECTS)
                .build(),
            projects,
            keys: api_keys,
        }
        .render()?,
    )
    .into_response())
}

async fn project_page(
    State(Settings { base_path, .. }): State<Settings>,
    State(navbar_template): State<NavbarBuilder>,
    DbConn(db_conn): DbConn,
    Path((team_id, project_id)): Path<(Uuid, Uuid)>,
    CurrentUser(current_user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
    let team = guards::require_team_membership(&current_user, &team_id, &db_conn).await?;

    let project = db_conn
        .interact(move |conn| {
            match Project::all()
                .filter(Project::with_id(&project_id))
                .filter(Project::with_team(&team_id))
                .first(conn)
            {
                diesel::QueryResult::Err(diesel::NotFound) => Err(AppError::NotFoundError(
                    "Project with that team and ID not found.".to_string(),
                )),
                other => other
                    .context("failed to load project")
                    .map_err(|err| err.into()),
            }
        })
        .await
        .unwrap()?;

    let selected_channels_query = project.selected_channels();
    let enabled_channel_ids: HashSet<Uuid> = db_conn
        .interact(move |conn| selected_channels_query.load(conn))
        .await
        .unwrap()
        .context("failed to load selected channels")?
        .iter()
        .map(|channel| channel.id)
        .collect();

    let team_channels = db_conn
        .interact(move |conn| {
            Channel::all()
                .filter(Channel::with_team(&team_id))
                .load(conn)
        })
        .await
        .unwrap()
        .context("failed to load team channels")?;

    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
    #[derive(Template)]
    #[template(path = "project.html")]
    struct ResponseTemplate {
        base_path: String,
        breadcrumbs: BreadcrumbTrail,
        csrf_token: String,
        enabled_channel_ids: HashSet<Uuid>,
        navbar: Navbar,
        project: Project,
        team_channels: Vec<Channel>,
    }
    Ok(Html(
        ResponseTemplate {
            breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
                .with_i18n_slug("en")
                .push_slug("Teams", "teams")
                .push_slug(&team.name, &team.id.simple().to_string())
                .push_slug("Projects", "projects")
                .push_slug(&project.name, &project.id.simple().to_string()),
            base_path,
            csrf_token,
            enabled_channel_ids,
            project,
            navbar: navbar_template
                .with_param("team_id", &team.id.simple().to_string())
                .with_active_item(NAVBAR_ITEM_PROJECTS)
                .build(),
            team_channels,
        }
        .render()?,
    ))
}

#[derive(Deserialize)]
struct UpdateEnabledChannelsFormBody {
    csrf_token: String,
    #[serde(default)]
    enabled_channels: Vec<Uuid>,
}

async fn update_enabled_channels(
    State(Settings { base_path, .. }): State<Settings>,
    DbConn(db_conn): DbConn,
    Path((team_id, project_id)): Path<(Uuid, Uuid)>,
    CurrentUser(current_user): CurrentUser,
    Form(form_body): Form<UpdateEnabledChannelsFormBody>,
) -> Result<impl IntoResponse, AppError> {
    guards::require_valid_csrf_token(&form_body.csrf_token, &current_user, &db_conn).await?;
    guards::require_team_membership(&current_user, &team_id, &db_conn).await?;

    db_conn
        .interact(move |conn| -> Result<(), AppError> {
            let project = match Project::all()
                .filter(Project::with_id(&project_id))
                .filter(Project::with_team(&team_id))
                .first(conn)
            {
                diesel::QueryResult::Err(diesel::NotFound) => Err(AppError::NotFoundError(
                    "Project with that team and ID not found.".to_string(),
                )),
                other => other
                    .context("failed to load project")
                    .map_err(|err| err.into()),
            }?;
            diesel::delete(
                channel_selections::table
                    .filter(ChannelSelection::with_project(&project.id))
                    .filter(channel_selections::channel_id.ne_all(&form_body.enabled_channels)),
            )
            .execute(conn)
            .context("failed to remove unset channel selections")?;
            for channel_id in form_body.enabled_channels {
                diesel::insert_into(channel_selections::table)
                    .values((
                        channel_selections::project_id.eq(&project.id),
                        channel_selections::channel_id.eq(channel_id),
                    ))
                    .on_conflict_do_nothing()
                    .execute(conn)
                    .context("failed to insert channel selections")?;
            }
            Ok(())
        })
        .await
        .unwrap()?;

    Ok(Redirect::to(&format!(
        "{}/en/teams/{}/projects/{}",
        base_path, team_id, project_id
    ))
    .into_response())
}
