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 rand::Rng as _;
use regex::Regex;
use serde::Deserialize;
use uuid::Uuid;

use crate::{
    app_error::AppError,
    app_state::{AppState, DbConn},
    channels::{BackendConfig, Channel, EmailBackendConfig, CHANNEL_BACKEND_EMAIL},
    csrf::generate_csrf_token,
    email::{MailSender as _, Mailer},
    guards,
    nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_CHANNELS},
    schema::channels,
    settings::Settings,
    users::CurrentUser,
};

const VERIFICATION_CODE_LEN: usize = 6;

/// Helper function to query a channel from the database by ID and team, and
/// return an appropriate error if no such channel exists.
fn get_channel_by_params<'a>(
    conn: &mut PgConnection,
    team_id: &'a Uuid,
    channel_id: &'a Uuid,
) -> Result<Channel, AppError> {
    match Channel::all()
        .filter(Channel::with_id(channel_id))
        .filter(Channel::with_team(team_id))
        .first(conn)
    {
        diesel::QueryResult::Err(diesel::result::Error::NotFound) => Err(AppError::NotFoundError(
            "Channel with that team and ID not found.".to_string(),
        )),
        diesel::QueryResult::Err(err) => Err(err.into()),
        diesel::QueryResult::Ok(channel) => Ok(channel),
    }
}

pub fn new_router() -> Router<AppState> {
    Router::new()
        .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))
}

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

    let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
    #[derive(Template)]
    #[template(path = "channels.html")]
    struct ResponseTemplate {
        base_path: String,
        breadcrumbs: BreadcrumbTrail,
        channels: Vec<Channel>,
        csrf_token: String,
        navbar: Navbar,
    }
    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("Channels", "channels"),
            base_path,
            channels,
            csrf_token,
            navbar: navbar_template
                .with_param("team_id", &team.id.simple().to_string())
                .with_active_item(NAVBAR_ITEM_CHANNELS)
                .build(),
        }
        .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_id = Uuid::now_v7();
    let channel = match form_body.channel_type.as_str() {
        CHANNEL_BACKEND_EMAIL => db_conn
            .interact::<_, Result<Channel, AppError>>(move |conn| {
                Ok(diesel::insert_into(channels::table)
                    .values((
                        channels::id.eq(channel_id),
                        channels::team_id.eq(team_id),
                        channels::name.eq("Untitled Email Channel"),
                        channels::backend_config
                            .eq(Into::<BackendConfig>::into(EmailBackendConfig::default())),
                    ))
                    .returning(Channel::as_returning())
                    .get_result(conn)
                    .context("Failed to insert new EmailChannel.")?)
            })
            .await
            .unwrap()?,
        _ => {
            return Err(AppError::BadRequestError(
                "Channel type not recognized.".to_string(),
            ));
        }
    };

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

async fn channel_page(
    State(Settings { base_path, .. }): State<Settings>,
    State(navbar_template): State<NavbarBuilder>,
    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 channel = {
        match db_conn
            .interact(move |conn| {
                Channel::all()
                    .filter(Channel::with_id(&channel_id))
                    .filter(Channel::with_team(&team_id))
                    .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)).await?;

    match channel.backend_config {
        BackendConfig::Email(_) => {
            #[derive(Template)]
            #[template(path = "channel-email.html")]
            struct ResponseTemplate {
                base_path: String,
                breadcrumbs: BreadcrumbTrail,
                channel: Channel,
                csrf_token: String,
                navbar: Navbar,
            }
            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("Channels", "channels")
                        .push_slug(&channel.name, &channel.id.simple().to_string()),
                    base_path,
                    channel,
                    csrf_token,
                    navbar: navbar_template
                        .with_param("team_id", &team.id.simple().to_string())
                        .with_active_item(NAVBAR_ITEM_CHANNELS)
                        .build(),
                }
                .render()?,
            ))
        }
        BackendConfig::Slack(_) => {
            Err(anyhow::anyhow!("Slack channel config page is not yet implemented.").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 updated_rows = {
        db_conn
            .interact(move |conn| {
                diesel::update(
                    channels::table
                        .filter(Channel::with_id(&channel_id))
                        .filter(Channel::with_team(&team_id)),
                )
                .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!(
        "{}/en/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(&rand::distributions::Uniform::from(0..9))
        .take(VERIFICATION_CODE_LEN)
        .map(|n| n.to_string())
        .collect();

    {
        let verification_code = verification_code.clone();
        let recipient = form_body.recipient.clone();
        db_conn
            .interact(move |conn| {
                // TODO: transaction retries
                conn.transaction::<_, AppError, _>(move |conn| {
                    let channel = get_channel_by_params(conn, &team_id, &channel_id)?;
                    let new_config = BackendConfig::Email(EmailBackendConfig {
                        recipient,
                        verification_code,
                        verification_code_guesses: 0,
                        ..channel.backend_config.try_into()?
                    });
                    let num_rows = diesel::update(channels::table.filter(Channel::with_id(&channel.id)))
                        .set(channels::backend_config.eq(new_config))
                        .execute(conn)?;
                    if num_rows != 1 {
                        return Err(anyhow::anyhow!(
                            "Updating EmailChannel recipient, the channel was found but {} rows were updated.",
                            num_rows
                        )
                        .into());
                    }
                    Ok(())
                })
            })
            .await
            .unwrap()?;
    }

    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,
        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.remove(0)?;

    Ok(Redirect::to(&format!(
        "{}/en/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?;

    if form_body.code.len() != VERIFICATION_CODE_LEN {
        return Err(AppError::BadRequestError(format!(
            "Verification code must be {} characters long.",
            VERIFICATION_CODE_LEN
        )));
    }

    {
        let verification_code = form_body.code;
        db_conn
            .interact(move |conn| {
                conn.transaction::<(), AppError, _>(move |conn| {
                    let channel = get_channel_by_params(conn, &team_id, &channel_id)?;
                    let config: EmailBackendConfig = channel.backend_config.try_into()?;
                    if config.verified {
                        return Err(AppError::BadRequestError(
                            "Channel's email address is already verified.".to_string(),
                        ));
                    }
                    const MAX_VERIFICATION_GUESSES: u32 = 100;
                    if config.verification_code_guesses > MAX_VERIFICATION_GUESSES {
                        return Err(AppError::BadRequestError(
                            "Verification expired.".to_string(),
                        ));
                    }
                    let new_config = if config.verification_code == verification_code {
                        EmailBackendConfig {
                            verified: true,
                            verification_code: "".to_string(),
                            verification_code_guesses: 0,
                            ..config
                        }
                    } else {
                        EmailBackendConfig {
                            verification_code_guesses: config.verification_code_guesses + 1,
                            ..config
                        }
                    };
                    diesel::update(channels::table.filter(Channel::with_id(&channel_id)))
                        .set(channels::backend_config.eq(Into::<BackendConfig>::into(new_config)))
                        .execute(conn)?;
                    Ok(())
                })
            })
            .await
            .unwrap()?;
    };

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