use std::collections::HashSet;

use anyhow::{anyhow, Context};
use askama_axum::Template;
use axum::{
    extract::{Path, State},
    response::{Html, IntoResponse, Redirect},
    routing::{get, post},
    Router,
};
use axum_extra::extract::Form;
use diesel::{delete, dsl::insert_into, prelude::*, update};
use rand::{distributions::Uniform, Rng};
use regex::Regex;
use serde::Deserialize;
use tower::ServiceBuilder;
use tower_http::{
    compression::CompressionLayer,
    services::{ServeDir, ServeFile},
    trace::TraceLayer,
};
use uuid::Uuid;

use crate::{
    api_keys::ApiKey,
    app_error::AppError,
    app_state::{AppState, DbConn},
    auth,
    channel_selections::ChannelSelection,
    channels::Channel,
    csrf::generate_csrf_token,
    email::{MailSender as _, Mailer},
    guards,
    nav_state::{Breadcrumb, NavState},
    projects::Project,
    schema::{self, channel_selections, channels, email_channels},
    settings::Settings,
    team_memberships::TeamMembership,
    teams::Team,
    users::CurrentUser,
    v0_router,
};

pub fn new_router(state: AppState) -> Router<()> {
    let base_path = state.settings.base_path.clone();
    Router::new().nest(
        base_path.as_str(),
        Router::new()
            .route("/", get(landing_page))
            .merge(v0_router::new_router(state.clone()))
            .route("/teams", get(teams_page))
            .route("/teams/{team_id}", get(team_page))
            .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),
            )
            .route("/teams/{team_id}/new-api-key", post(post_new_api_key))
            .route("/teams/{team_id}/channels", get(channels_page))
            .route("/teams/{team_id}/channels/{channel_id}", get(channel_page))
            .route(
                "/teams/{team_id}/channels/{channel_id}/update-channel",
                post(update_channel),
            )
            .route(
                "/teams/{team_id}/channels/{channel_id}/update-email-recipient",
                post(update_channel_email_recipient),
            )
            .route(
                "/teams/{team_id}/channels/{channel_id}/verify-email",
                post(verify_email),
            )
            .route("/teams/{team_id}/new-channel", post(post_new_channel))
            .route("/new-team", get(new_team_page))
            .route("/new-team", post(post_new_team))
            .nest("/auth", auth::new_router())
            .fallback_service(
                ServeDir::new("static").not_found_service(ServeFile::new("static/_404.html")),
            )
            .layer(
                ServiceBuilder::new()
                    .layer(TraceLayer::new_for_http())
                    .layer(CompressionLayer::new()),
            )
            .with_state(state),
    )
}

async fn landing_page(State(state): State<AppState>) -> impl IntoResponse {
    Redirect::to(&format!("{}/teams", state.settings.base_path))
}

async fn teams_page(
    State(Settings { base_path, .. }): State<Settings>,
    DbConn(conn): DbConn,
    CurrentUser(current_user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
    let team_memberships_query = current_user.clone().team_memberships();
    let teams: Vec<Team> = conn
        .interact(move |conn| team_memberships_query.load(conn))
        .await
        .unwrap()
        .context("failed to load team memberships")
        .map(|memberships| memberships.into_iter().map(|(_, team)| team).collect())?;
    let nav_state = NavState::new()
        .set_base_path(&base_path)
        .push_slug(Breadcrumb {
            href: "teams".to_string(),
            label: "New Team".to_string(),
        })
        .set_navbar_active_item("teams");
    #[derive(Template)]
    #[template(path = "teams.html")]
    struct ResponseTemplate {
        base_path: String,
        teams: Vec<Team>,
        nav_state: NavState,
    }
    Ok(Html(
        ResponseTemplate {
            base_path,
            nav_state,
            teams,
        }
        .render()?,
    ))
}

async fn team_page(State(state): State<AppState>, Path(team_id): Path<Uuid>) -> impl IntoResponse {
    Redirect::to(&format!(
        "{}/teams/{}/projects",
        state.settings.base_path, team_id
    ))
}

#[derive(Deserialize)]
struct PostNewApiKeyForm {
    csrf_token: String,
}

async fn post_new_api_key(
    State(Settings { base_path, .. }): State<Settings>,
    DbConn(db_conn): DbConn,
    Path(team_id): Path<Uuid>,
    CurrentUser(current_user): CurrentUser,
    Form(form): Form<PostNewApiKeyForm>,
) -> Result<impl IntoResponse, AppError> {
    guards::require_valid_csrf_token(&form.csrf_token, &current_user, &db_conn).await?;
    let team = guards::require_team_membership(&current_user, &team_id, &db_conn).await?;

    ApiKey::generate_for_team(&db_conn, team.id.clone()).await?;
    Ok(Redirect::to(&format!(
        "{}/teams/{}/projects",
        base_path,
        team.id.hyphenated().to_string()
    ))
    .into_response())
}

async fn new_team_page(
    State(Settings { base_path, .. }): State<Settings>,
    DbConn(db_conn): DbConn,
    CurrentUser(current_user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;

    let nav_state = NavState::new()
        .set_base_path(&base_path)
        .push_slug(Breadcrumb {
            href: "new-team".to_string(),
            label: "New Team".to_string(),
        })
        .set_navbar_active_item("teams");
    #[derive(Template)]
    #[template(path = "new-team.html")]
    struct ResponseTemplate {
        base_path: String,
        csrf_token: String,
        nav_state: NavState,
    }
    Ok(Html(
        ResponseTemplate {
            base_path,
            csrf_token,
            nav_state,
        }
        .render()?,
    ))
}

#[derive(Deserialize)]
struct PostNewTeamForm {
    name: String,
    csrf_token: String,
}

async fn post_new_team(
    DbConn(db_conn): DbConn,
    State(Settings { base_path, .. }): State<Settings>,
    CurrentUser(current_user): CurrentUser,
    Form(form): Form<PostNewTeamForm>,
) -> Result<impl IntoResponse, AppError> {
    guards::require_valid_csrf_token(&form.csrf_token, &current_user, &db_conn).await?;

    let team_id = Uuid::now_v7();
    let team = Team {
        id: team_id.clone(),
        name: form.name,
    };
    let team_membership = TeamMembership {
        team_id: team_id.clone(),
        user_id: current_user.id,
        roles: vec![Some("OWNER".to_string())],
    };
    db_conn
        .interact(move |conn| {
            conn.transaction(move |conn| {
                insert_into(schema::teams::table)
                    .values(team)
                    .execute(conn)?;
                insert_into(schema::team_memberships::table)
                    .values(team_membership)
                    .execute(conn)?;
                diesel::QueryResult::Ok(())
            })
        })
        .await
        .unwrap()
        .unwrap();
    ApiKey::generate_for_team(&db_conn, team_id.clone()).await?;
    Ok(Redirect::to(&format!("{}/teams/{}/projects", base_path, team_id)).into_response())
}

async fn projects_page(
    State(Settings { base_path, .. }): State<Settings>,
    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_query = team.clone().api_keys();
    let (api_keys, projects) = db_conn
        .interact(move |conn| {
            diesel::QueryResult::Ok((api_keys_query.load(conn)?, Project::all().load(conn)?))
        })
        .await
        .unwrap()?;

    #[derive(Template)]
    #[template(path = "projects.html")]
    struct ResponseTemplate {
        base_path: String,
        csrf_token: String,
        keys: Vec<ApiKey>,
        nav_state: NavState,
        projects: Vec<Project>,
    }
    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id.clone())).await?;
    let nav_state = NavState::new()
        .set_base_path(&base_path)
        .push_team(&team)
        .push_slug(Breadcrumb {
            href: "projects".to_string(),
            label: "Projects".to_string(),
        })
        .set_navbar_active_item("projects");
    Ok(Html(
        ResponseTemplate {
            base_path,
            csrf_token,
            nav_state,
            projects,
            keys: api_keys,
        }
        .render()?,
    )
    .into_response())
}

async fn channels_page(
    State(Settings { base_path, .. }): State<Settings>,
    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 team_filter = Channel::with_team(team_id);
    let channels = db_conn
        .interact(move |conn| Channel::all().filter(team_filter).load(conn))
        .await
        .unwrap()
        .context("Failed to load channels list.")?;

    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id.clone())).await?;
    let nav_state = NavState::new()
        .set_base_path(&base_path)
        .push_team(&team)
        .push_slug(Breadcrumb {
            href: "channels".to_string(),
            label: "Channels".to_string(),
        })
        .set_navbar_active_item("channels");
    #[derive(Template)]
    #[template(path = "channels.html")]
    struct ResponseTemplate {
        base_path: String,
        channels: Vec<Channel>,
        csrf_token: String,
        nav_state: NavState,
    }
    Ok(Html(
        ResponseTemplate {
            base_path,
            channels,
            csrf_token,
            nav_state,
        }
        .render()?,
    )
    .into_response())
}

#[derive(Deserialize)]
struct NewChannelPostFormBody {
    csrf_token: String,
    channel_type: String,
}

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

    let channel = match form_body.channel_type.as_str() {
        "email" => Channel::create_email_channel(&db_conn, team.id.clone()).await?,
        _ => {
            return Err(AppError::BadRequestError(
                "Channel type not recognized.".to_string(),
            ));
        }
    };

    Ok(Redirect::to(&format!(
        "{}/teams/{}/channels/{}",
        base_path,
        team.id.simple(),
        channel.id.simple()
    )))
}

async fn channel_page(
    State(Settings { base_path, .. }): State<Settings>,
    DbConn(db_conn): DbConn,
    Path((team_id, channel_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 id_filter = Channel::with_id(channel_id);
    let team_filter = Channel::with_team(team_id.clone());
    let channel = match db_conn
        .interact(move |conn| {
            Channel::all()
                .filter(id_filter)
                .filter(team_filter)
                .first(conn)
                .optional()
        })
        .await
        .unwrap()?
    {
        None => {
            return Err(AppError::NotFoundError(
                "Channel with that team and ID not found".to_string(),
            ));
        }
        Some(channel) => channel,
    };

    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id.clone())).await?;
    let nav_state = NavState::new()
        .set_base_path(&base_path)
        .push_team(&team)
        .push_slug(Breadcrumb {
            href: "channels".to_string(),
            label: "Channels".to_string(),
        })
        .push_slug(Breadcrumb {
            href: channel.id.simple().to_string(),
            label: channel.name.clone(),
        })
        .set_navbar_active_item("channels");

    if channel.email_data.is_some() {
        #[derive(Template)]
        #[template(path = "channel-email.html")]
        struct ResponseTemplate {
            base_path: String,
            channel: Channel,
            csrf_token: String,
            nav_state: NavState,
        }
        Ok(Html(
            ResponseTemplate {
                base_path,
                channel,
                csrf_token,
                nav_state,
            }
            .render()?,
        ))
    } else if channel.slack_data.is_some() {
        Err(anyhow::anyhow!("Slack channel config page is not yet implemented.").into())
    } else {
        Err(anyhow::anyhow!(
            "Channel doesn't have a recognized variant for which to render a config page."
        )
        .into())
    }
}

#[derive(Deserialize)]
struct UpdateChannelFormBody {
    csrf_token: String,
    name: String,
    enable_by_default: Option<String>,
}

async fn update_channel(
    State(Settings { base_path, .. }): State<Settings>,
    DbConn(db_conn): DbConn,
    Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
    CurrentUser(current_user): CurrentUser,
    Form(form_body): Form<UpdateChannelFormBody>,
) -> 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?;

    let id_filter = Channel::with_id(channel_id.clone());
    let team_filter = Channel::with_team(team_id.clone());
    let updated_rows = db_conn
        .interact(move |conn| {
            update(channels::table.filter(id_filter).filter(team_filter))
                .set((
                    channels::name.eq(form_body.name),
                    channels::enable_by_default
                        .eq(form_body.enable_by_default.unwrap_or("false".to_string()) == "true"),
                ))
                .execute(conn)
        })
        .await
        .unwrap()
        .context("Failed to load Channel while updating.")?;
    if updated_rows != 1 {
        return Err(AppError::NotFoundError(
            "Channel with that team and ID not found".to_string(),
        ));
    }
    Ok(Redirect::to(&format!(
        "{}/teams/{}/channels/{}",
        base_path,
        team_id.simple(),
        channel_id.simple()
    ))
    .into_response())
}

#[derive(Deserialize)]
struct UpdateChannelEmailRecipientFormBody {
    // Yes it's a mouthful, but it's only used twice
    csrf_token: String,
    recipient: String,
}

async fn update_channel_email_recipient(
    State(Settings {
        base_path,
        email: email_settings,
        ..
    }): State<Settings>,
    DbConn(db_conn): DbConn,
    State(mailer): State<Mailer>,
    Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
    CurrentUser(current_user): CurrentUser,
    Form(form_body): Form<UpdateChannelEmailRecipientFormBody>,
) -> 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?;

    if !is_permissible_email(&form_body.recipient) {
        return Err(AppError::BadRequestError(
            "Unable to validate email address format.".to_string(),
        ));
    }

    let verification_code: String = rand::thread_rng()
        .sample_iter(&Uniform::try_from(0..9).unwrap())
        .take(6)
        .map(|n| n.to_string())
        .collect();

    let verification_code_copy = verification_code.clone();
    let recipient_copy = form_body.recipient.clone();

    let channel_id_filter = Channel::with_id(channel_id.clone());
    let email_channel_id_filter = email_channels::id.eq(channel_id.clone());
    let team_filter = Channel::with_team(team_id.clone());
    let updated_rows = db_conn
        .interact(move |conn| {
            if Channel::all()
                .filter(channel_id_filter)
                .filter(team_filter)
                .filter(email_channel_id_filter.clone())
                .first(conn)
                .optional()
                .context("failed to check whether channel exists under team")
                .map_err(Into::<AppError>::into)?
                .is_none()
            {
                return Err(AppError::NotFoundError(
                    "Channel with that team and ID not found.".to_string(),
                ));
            }
            update(email_channels::table.filter(email_channel_id_filter))
                .set((
                    email_channels::recipient.eq(recipient_copy),
                    email_channels::verification_code.eq(verification_code_copy),
                    email_channels::verification_code_guesses.eq(0),
                ))
                .execute(conn)
                .map_err(Into::<AppError>::into)
        })
        .await
        .unwrap()?;
    if updated_rows != 1 {
        return Err(anyhow!(
            "Updating EmailChannel recipient, the channel was found but {} rows were updated.",
            updated_rows
        )
        .into());
    }

    tracing::debug!(
        "Email verification code for {} is: {}",
        form_body.recipient,
        verification_code
    );
    tracing::info!(
        "Sending email verification code to: {}",
        form_body.recipient
    );
    let email = crate::email::Message {
        from: email_settings.verification_from.into(),
        to: form_body.recipient.parse()?,
        subject: "Verify Your Email".to_string(),
        text_body: format!("Your email verification code is: {}", verification_code),
    };
    mailer.send_batch(vec![email]).await?;

    Ok(Redirect::to(&format!(
        "{}/teams/{}/channels/{}",
        base_path,
        team_id.simple(),
        channel_id.simple()
    )))
}

/**
 * Returns true if the email address matches a format recognized as "valid".
 * Not all "legal" email addresses will be accepted, but addresses that are
 * "illegal" and/or could result in unexpected behavior should be rejected.
 */
fn is_permissible_email(address: &str) -> bool {
    let re = Regex::new(r"^[a-zA-Z0-9._+-]+@([a-zA-Z0-9_-]+.)+[a-zA-Z]+$")
        .expect("email validation regex should parse");
    re.is_match(address)
}

#[derive(Deserialize)]
struct VerifyEmailFormBody {
    csrf_token: String,
    code: String,
}

async fn verify_email(
    State(Settings { base_path, .. }): State<Settings>,
    DbConn(db_conn): DbConn,
    Path((team_id, channel_id)): Path<(Uuid, Uuid)>,
    CurrentUser(current_user): CurrentUser,
    Form(form_body): Form<VerifyEmailFormBody>,
) -> 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?;

    let channel_id_filter = Channel::with_id(channel_id.clone());
    let email_channel_id_filter = email_channels::id.eq(channel_id.clone());
    let team_filter = Channel::with_team(team_id.clone());
    let updated_rows = db_conn
        .interact(move |conn| {
            if Channel::all()
                .filter(channel_id_filter)
                .filter(team_filter)
                .filter(email_channel_id_filter.clone())
                .first(conn)
                .optional()
                .context("failed to check whether channel exists under team")
                .map_err(Into::<AppError>::into)?
                .is_none()
            {
                return Err(AppError::NotFoundError(
                    "Channel with that team and ID not found.".to_string(),
                ));
            }
            update(email_channels::table.filter(email_channel_id_filter))
                .set(
                    email_channels::verification_code_guesses
                        .eq(email_channels::verification_code_guesses + 1),
                )
                .execute(conn)
                .map_err(Into::<AppError>::into)?;
            update(
                email_channels::table
                    .filter(email_channel_id_filter)
                    .filter(email_channels::verification_code.eq(form_body.code)),
            )
            .set(email_channels::verified.eq(true))
            .execute(conn)
            .map_err(Into::<AppError>::into)
        })
        .await
        .unwrap()?;
    if updated_rows != 1 {
        return Err(AppError::BadRequestError(
            "Verification code not accepted.".to_string(),
        ));
    }

    Ok(Redirect::to(&format!(
        "{}/teams/{}/channels/{}",
        base_path,
        team_id.simple(),
        channel_id.simple()
    )))
}

async fn project_page(
    State(Settings { base_path, .. }): State<Settings>,
    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_id_filter = Project::with_id(project_id.clone());
    let project_team_filter = Project::with_team(team_id.clone());
    let project = db_conn
        .interact(move |conn| {
            match Project::all()
                .filter(project_id_filter)
                .filter(project_team_filter)
                .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_filter = Channel::with_team(team.id.clone());
    let team_channels = db_conn
        .interact(move |conn| Channel::all().filter(team_filter).load(conn))
        .await
        .unwrap()
        .context("failed to load team channels")?;

    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
    let nav_state = NavState::new()
        .set_base_path(&base_path)
        .push_team(&team)
        .push_project(&project)?;

    #[derive(Template)]
    #[template(path = "project.html")]
    struct ResponseTemplate {
        base_path: String,
        csrf_token: String,
        enabled_channel_ids: HashSet<Uuid>,
        nav_state: NavState,
        project: Project,
        team_channels: Vec<Channel>,
    }
    Ok(Html(
        ResponseTemplate {
            base_path,
            csrf_token,
            enabled_channel_ids,
            project,
            nav_state,
            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?;

    let id_filter = Project::with_id(project_id.clone());
    let team_filter = Project::with_team(team_id.clone());
    db_conn
        .interact(move |conn| -> Result<(), AppError> {
            let project = match Project::all()
                .filter(id_filter)
                .filter(team_filter)
                .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()),
            }?;
            delete(
                channel_selections::table
                    .filter(ChannelSelection::with_project(project.id.clone()))
                    .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 {
                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!(
        "{}/teams/{}/projects/{}",
        base_path, team_id, project_id
    ))
    .into_response())
}
