use crate::config::Color;
use crate::permissions::Perm;
use crate::{fallback, get_user_login, get_user_login_email, get_user_login_email_strict, Config};
use axum::{
    debug_handler,
    extract::{ConnectInfo, Path, Query, State},
    response::{IntoResponse, Redirect, Response},
    routing::{get, post},
    Form, Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use axum_extra::TypedHeader;
use chrono::{DateTime, Utc};
use diesel::{
    BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods,
    OptionalExtension, QueryDsl,
};
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::AsyncPgConnection;
use diesel_async::{AsyncConnection, RunQueryDsl};
use crate::repository::TreePath;

use http::StatusCode;
use libpijul::changestore::ChangeStore;
use libpijul::Base32;
use serde_derive::*;
use std::net::SocketAddr;
use tracing::*;

pub fn router() -> Router<Config> {
    Router::new()
        .route("/{owner}/{repo}", get(list))
        .route("/{owner}/{repo}/new", get(new).post(new_post))
        .route(
            "/{owner}/{repo}/{disc}",
            get(discussion).post(discussion_post),
        )
        .route("/{owner}/{repo}/{disc}/remove_change", post(remove_change))
        .route("/{owner}/{repo}/{disc}/add_change", post(add_change))
        .fallback(fallback)
}

#[derive(Debug, Deserialize)]
pub struct DiscQuery {
    from: Option<u64>,
    closed: Option<bool>,
    q: Option<String>,
}

pub async fn list(
    State(config): State<Config>,
    jar: SignedCookieJar,
    Path(tree): Path<TreePath>,
    Query(q): Query<DiscQuery>,
) -> Result<Response, crate::Error> {
    debug!("list {:?} {:?}", tree, q);
    let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {
        (Some(a), Some(b))
    } else {
        (None, None)
    };
    let mut db = config.db.get().await?;
    let (id, _) =
        crate::repository::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
    if let Some(_) = q.q {
        unimplemented!()
    } else {
        use crate::db::discussion_tags::dsl as dt;
        use crate::db::discussions::dsl as d;
        use crate::db::tags::dsl as tags;
        use crate::db::users::dsl as users;
        use diesel::dsl::count;

        let n = d::discussions
            .filter(d::repository_id.eq(id))
            .select(count(d::id))
            .get_result::<i64>(&mut db)
            .await?;

        let mut max_number = 0;
        let mut disc: Vec<_> = (if let Some(true) = q.closed {
            d::discussions
                .inner_join(users::users)
                .filter(d::repository_id.eq(id))
                .select((
                    d::id,
                    d::number,
                    d::title,
                    users::login,
                    d::creation_date,
                    d::changes,
                    d::closed,
                ))
                .order_by(d::number)
                .offset(q.from.unwrap_or(0) as i64)
                .limit(LIMIT)
                .get_results::<(
                    uuid::Uuid,
                    i32,
                    String,
                    String,
                    DateTime<Utc>,
                    i32,
                    Option<DateTime<Utc>>,
                )>(&mut db)
                .await?
        } else {
            d::discussions
                .inner_join(users::users)
                .filter(d::repository_id.eq(id))
                .filter(d::closed.is_null())
                .select((
                    d::id,
                    d::number,
                    d::title,
                    users::login,
                    d::creation_date,
                    d::changes,
                    d::closed,
                ))
                .order_by(d::number)
                .offset(q.from.unwrap_or(0) as i64)
                .limit(LIMIT)
                .get_results::<(
                    uuid::Uuid,
                    i32,
                    String,
                    String,
                    DateTime<Utc>,
                    i32,
                    Option<DateTime<Utc>>,
                )>(&mut db)
                .await?
        })
        .into_iter()
        .map(|(id, num, title, author, opened, changes, closed)| {
            max_number = max_number.max(num);
            DiscussionListItem {
                id,
                num,
                title,
                author,
                opened,
                changes,
                closed,
                ..DiscussionListItem::default()
            }
        })
        .collect();

        let mut i = 0;
        for (num, tag) in d::discussions
            .inner_join(dt::discussion_tags)
            .inner_join(tags::tags.on(dt::tag.eq(tags::id.nullable())))
            .filter(d::number.ge(q.from.unwrap_or(0) as i32))
            .filter(d::number.le(max_number))
            .filter(tags::id.is_not_null())
            .select((d::number, tags::id))
            .get_results::<(i32, uuid::Uuid)>(&mut db)
            .await?
        {
            while i < disc.len() && num < disc[i].num {
                i += 1
            }
            if i >= disc.len() {
                break;
            }
            disc[i].tags.push(tag)
        }

        Ok(Json(Discussions {
            owner: tree.owner,
            repo: tree.repo,
            login,
            discussions: disc,
            n,
        })
        .into_response())
    }
}

#[derive(Debug, Serialize)]
struct Discussions {
    owner: String,
    repo: String,
    n: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    login: Option<String>,
    discussions: Vec<DiscussionListItem>,
}

#[derive(Debug, Deserialize)]
pub struct DiscPath {
    owner: String,
    repo: String,
    disc: i32,
}

const LIMIT: i64 = 20;

#[derive(Debug, Default, Serialize)]
struct DiscussionListItem {
    id: uuid::Uuid,
    num: i32,
    title: String,
    author: String,
    opened: DateTime<Utc>,
    changes: i32,
    tags: Vec<uuid::Uuid>,
    #[serde(skip_serializing_if = "Option::is_none")]
    closed: Option<DateTime<Utc>>,
}

#[debug_handler]
pub async fn new(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    Path(tree): Path<TreePath>,
) -> Result<Response, crate::Error> {
    debug!("new {:?}", tree);
    let (uid, login) = if let Some((a, b)) = get_user_login(&jar, &config).await? {
        (Some(a), Some(b))
    } else {
        (None, None)
    };
    let mut db = config.db.get().await?;
    crate::repository::repository_id(
        &mut db,
        &tree.owner,
        &tree.repo,
        uid,
        Perm::CREATE_DISCUSSION,
    )
    .await?;
    let auth_token = token.authenticity_token().ok();
    Ok((
        token,
        Json(NewDiscussion {
            owner: tree.owner,
            repo: tree.repo,
            login,
            unique: rand::random(),
            token: auth_token,
        }),
    )
        .into_response())
}

#[derive(Debug, Serialize)]
struct NewDiscussion {
    owner: String,
    repo: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    login: Option<String>,
    unique: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    token: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct NewDiscussionForm {
    new_discussion_name: String,
    new_discussion_comment: String,
    uniq: i64,
    token: String,
}

#[debug_handler]
pub async fn new_post(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
    Path(tree): Path<TreePath>,
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    Form(form): Form<NewDiscussionForm>,
) -> Result<Response, crate::Error> {
    token.verify(&form.token)?;
    debug!("new {:?}", tree);
    let (uid, login, email) = get_user_login_email_strict(&jar, &config).await?;
    let mut db = config.db.get().await?;
    let (id, _) = crate::repository::repository_id(
        &mut db,
        &tree.owner,
        &tree.repo,
        Some(uid),
        Perm::CREATE_DISCUSSION,
    )
    .await?;

    use crate::db::repositories::dsl as r;
    db.transaction(move |mut txn| {
        (async move {
            let number: i32 = diesel::update(r::repositories.find(id))
                .set(r::next_discussion_number.eq(r::next_discussion_number + 1))
                .returning(r::next_discussion_number)
                .get_result(&mut txn)
                .await?;

            use crate::db::discussions::dsl as d;
            let ip_sql =
                ipnetwork::IpNetwork::new(addr.ip(), if addr.is_ipv4() { 32 } else { 128 })
                    .unwrap();
            let disc_id: uuid::Uuid = diesel::insert_into(d::discussions)
                .values((
                    d::title.eq(&form.new_discussion_name.trim()),
                    d::author.eq(uid),
                    d::creation_ip.eq(&ip_sql),
                    d::repository_id.eq(id),
                    d::number.eq(number),
                    d::uniq.eq(&form.uniq),
                ))
                .returning(d::id)
                .get_result(&mut txn)
                .await?;

            use crate::db::comments::dsl as c;
            let comment = form.new_discussion_comment.trim();
            if !comment.is_empty() {
                let comment_html =
                    crate::markdown::render_markdown(&tree.owner, &tree.repo, comment);
                diesel::insert_into(c::comments)
                    .values((
                        c::discussion_id.eq(disc_id),
                        c::author.eq(uid),
                        c::creation_ip.eq(&ip_sql),
                        c::contents.eq(comment),
                        c::cached_html.eq(&comment_html.rendered),
                        c::uniq.eq(&form.uniq),
                    ))
                    .execute(&mut txn)
                    .await?;
            }

            use crate::db::discussion_subscriptions::dsl as ds;
            diesel::insert_into(ds::discussion_subscriptions)
                .values((ds::user_id.eq(uid), ds::discussion_id.eq(disc_id)))
                .execute(&mut txn)
                .await?;

            if let Some(TypedHeader(crate::config::AcceptJson(true))) = accept_json {
                Ok::<_, crate::Error>(
                    (
                        token.clone(),
                        Json(
                            discussion_json(
                                &config,
                                &mut txn,
                                id,
                                token,
                                Path(DiscPath {
                                    owner: tree.owner,
                                    repo: tree.repo,
                                    disc: number,
                                }),
                                Some(login),
                                Some(email),
                            )
                            .await?,
                        ),
                    )
                        .into_response(),
                )
            } else {
                Ok::<_, crate::Error>(
                    Redirect::to(&format!(
                        "/{}/{}/discussion/{}",
                        tree.owner, tree.repo, number
                    ))
                    .into_response(),
                )
            }
        })
        .scope_boxed()
    })
    .await
}

#[debug_handler]
pub async fn discussion(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    Path(tree): Path<DiscPath>,
) -> Result<Response, crate::Error> {
    debug!("discussion {:?}", tree);
    let (uid, login, email) = if let Some((a, b, c)) = get_user_login_email(&jar, &config).await? {
        (Some(a), Some(b), Some(c))
    } else {
        (None, None, None)
    };
    let mut db = config.db.get().await?;
    let (repo, _) =
        crate::repository::repository_id(&mut db, &tree.owner, &tree.repo, uid, Perm::READ).await?;
    Ok((
        token.clone(),
        Json(discussion_json(&config, &mut db, repo, token, Path(tree), login, email).await?),
    )
        .into_response())
}

async fn discussion_json(
    config: &Config,
    db: &mut AsyncPgConnection,
    id: uuid::Uuid,
    token: axum_csrf::CsrfToken,
    Path(tree): Path<DiscPath>,
    login: Option<String>,
    email: Option<String>,
) -> Result<Discussion, crate::Error> {
    use crate::db::discussions::dsl as d;
    use crate::db::users::dsl as users;

    let (disc_id, title, author, opened, closed) = d::discussions
        .inner_join(users::users)
        .filter(d::repository_id.eq(id))
        .filter(d::number.eq(tree.disc))
        .select((d::id, d::title, users::login, d::creation_date, d::closed))
        .get_result::<(_, _, _, DateTime<Utc>, Option<DateTime<Utc>>)>(db)
        .await?;

    use crate::db::comments::dsl as comments;
    use crate::db::discussion_changes::dsl as dc;
    use crate::db::discussion_tags::dsl as dt;
    use crate::db::tags::dsl as tags;

    let mut comments: Vec<_> = comments::comments
        .inner_join(users::users)
        .filter(comments::discussion_id.eq(disc_id))
        .select((
            comments::id,
            users::login,
            comments::creation_date,
            comments::contents,
            comments::cached_html,
        ))
        .order_by(comments::creation_date)
        .get_results::<(uuid::Uuid, String, DateTime<Utc>, String, String)>(db)
        .await?
        .into_iter()
        .map(
            |(id, author, date, content, content_html)| DiscussionItem::Comment {
                comment: Comment {
                    author,
                    id,
                    timestamp: date.timestamp(),
                    content,
                    content_html,
                },
            },
        )
        .collect();

    for (id, name, color, date, active, author, dt_id, added) in dt::discussion_tags
        .inner_join(users::users)
        .left_join(tags::tags)
        .filter(dt::discussion.eq(disc_id))
        .select((
            tags::id.nullable(),
            tags::name.nullable(),
            tags::color.nullable(),
            dt::date,
            dt::active,
            users::login,
            dt::id,
            dt::addition,
        ))
        .order_by(dt::date)
        .get_results::<(
            Option<uuid::Uuid>,
            Option<String>,
            Option<i32>,
            DateTime<Utc>,
            bool,
            String,
            uuid::Uuid,
            bool,
        )>(db)
        .await?
    {
        let tag = if let (Some(id), Some(name), Some(color)) = (id, name, color) {
            let color = Color(color);
            Some(TagT {
                name,
                id,
                color: color.to_string(),
                fg: color.fg().to_string(),
                active,
            })
        } else {
            None
        };
        comments.push(DiscussionItem::Tag {
            tag: Tag {
                author,
                id: dt_id,
                timestamp: date.timestamp(),
                tag,
                added,
            },
        })
    }

    for (pushed_by, hash, timestamp, change_id, removed) in dc::discussion_changes
        .inner_join(users::users)
        .select((users::login, dc::change, dc::added, dc::id, dc::removed))
        .get_results::<(
            String,
            String,
            DateTime<Utc>,
            uuid::Uuid,
            Option<DateTime<Utc>>,
        )>(db)
        .await?
    {
        let locks = config.repo_locks.clone();
        let repo = locks.get(&id).await?;
        let mut header = repo
            .changes
            .get_header(&libpijul::Hash::from_base32(hash.as_bytes()).unwrap())?;
        let hash_ = libpijul::Hash::from_base32(hash.as_bytes()).unwrap();

        comments.push(DiscussionItem::Patch {
            patch: Patch {
                can_add: if removed.is_some() {
                    can_add(&config, db, id, disc_id, hash_).await.is_ok()
                } else {
                    false
                },
                can_remove: if removed.is_none() {
                    can_remove(&config, db, id, disc_id, hash_).await.is_ok()
                } else {
                    false
                },
                authors: crate::change::get_authors(db, &mut header.authors).await?,
                header,
                pushed_by,
                hash,
                timestamp: timestamp.timestamp(),
                removed: removed.as_ref().map(DateTime::timestamp),
                id: change_id,
            },
        })
    }

    comments.sort_by_key(DiscussionItem::timestamp);

    let auth_token = token.authenticity_token().ok();
    Ok(Discussion {
        owner: tree.owner,
        repo: tree.repo,
        // channels,
        // default_channel,
        login,
        email,
        token: auth_token,
        uniq: rand::random(),
        d: DiscussionT {
            n: tree.disc,
            id: disc_id,
            title,
            author,
            closed: closed.map(|x| x.timestamp()),
            opened: opened.timestamp(),
        },
        comments,
    })
}

#[derive(Debug, Serialize)]
struct Discussion {
    owner: String,
    repo: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    login: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    token: Option<String>,
    // channels: Vec<String>,
    // default_channel: String,
    uniq: i64,
    d: DiscussionT,
    comments: Vec<DiscussionItem>,
}

#[derive(Debug, Serialize)]
struct DiscussionT {
    n: i32,
    id: uuid::Uuid,
    title: String,
    author: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    closed: Option<i64>,
    opened: i64,
}

#[derive(Debug, Serialize)]
#[serde(untagged)]
enum DiscussionItem {
    Comment { comment: Comment },
    Patch { patch: Patch },
    Tag { tag: Tag },
}

impl DiscussionItem {
    fn timestamp(&self) -> i64 {
        match self {
            DiscussionItem::Comment { comment } => comment.timestamp,
            DiscussionItem::Patch { patch } => patch.timestamp,
            DiscussionItem::Tag { tag } => tag.timestamp,
        }
    }
}

#[derive(Debug, Serialize)]
struct Comment {
    author: String,
    timestamp: i64,
    id: uuid::Uuid,
    content: String,
    content_html: String,
}

#[derive(Debug, Serialize)]
struct Tag {
    author: String,
    timestamp: i64,
    added: bool,
    id: uuid::Uuid,
    #[serde(skip_serializing_if = "Option::is_none")]
    tag: Option<TagT>,
}

#[derive(Debug, Serialize)]
struct TagT {
    id: uuid::Uuid,
    name: String,
    fg: String,
    color: String,
    active: bool,
}

#[derive(Debug, Serialize)]
struct Patch {
    can_add: bool,
    can_remove: bool,
    hash: String,
    timestamp: i64,
    pushed_by: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    removed: Option<i64>,
    header: libpijul::change::ChangeHeader,
    authors: Vec<crate::change::Author>,
    id: uuid::Uuid,
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
enum Action {
    #[serde(rename = "close")]
    Close,
    #[serde(rename = "reopen")]
    Reopen,
    #[serde(rename = "apply")]
    Apply,
    #[serde(rename = "unrecord")]
    Unrecord,
}

#[derive(Debug, Deserialize)]
pub struct RemoveChangeForm {
    discussion_change: uuid::Uuid,
    token: String,
}

#[axum::debug_handler]
async fn add_change(
    config: State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    tree: Path<DiscPath>,
    form: Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
    edit_change(true, config, jar, token, tree, form).await
}

#[axum::debug_handler]
async fn remove_change(
    config: State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    tree: Path<DiscPath>,
    form: Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
    edit_change(false, config, jar, token, tree, form).await
}

async fn edit_change(
    add: bool,
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    Path(tree): Path<DiscPath>,
    Form(form): Form<RemoveChangeForm>,
) -> Result<Response, crate::Error> {
    token.verify(&form.token)?;

    use crate::db::discussion_changes::dsl as dc;
    use crate::db::discussions::dsl as d;
    use crate::db::repositories::dsl as r;

    let uid = crate::get_user_id_strict(&jar)?;
    config
        .db
        .get()
        .await?
        .transaction(|mut txn| {
            (async move {
                // If the user has permissions, return the discussion id.
                let repo_disc = dc::discussion_changes
                    .find(form.discussion_change)
                    .inner_join(d::discussions)
                    .inner_join(r::repositories.on(d::repository_id.eq(r::id)))
                    .filter(dc::pushed_by.eq(uid).or(crate::has_permissions!(
                        uid,
                        r::id,
                        Perm::EDIT_DISCUSSION.bits()
                    )))
                    .filter(r::is_active)
                    .select((r::id, d::id, dc::change))
                    .get_result::<(uuid::Uuid, uuid::Uuid, String)>(&mut txn)
                    .await
                    .optional()?;

                let (repo_id, disc_id, hash) = if let Some(disc_id) = repo_disc {
                    disc_id
                } else {
                    return Ok(StatusCode::FORBIDDEN.into_response());
                };

                let hash = libpijul::Hash::from_base32(hash.as_bytes()).unwrap();

                if add {
                    can_add(&config, &mut txn, repo_id, disc_id, hash).await?;
                    diesel::update(d::discussions.find(disc_id))
                        .set(d::changes.eq(d::changes + 1))
                        .execute(&mut txn)
                        .await?;
                } else {
                    can_remove(&config, &mut txn, repo_id, disc_id, hash).await?;
                    diesel::update(d::discussions.find(disc_id))
                        .set(d::changes.eq(d::changes - 1))
                        .execute(&mut txn)
                        .await?;
                }

                diesel::update(dc::discussion_changes.find(form.discussion_change))
                    .set(dc::removed.eq(if add { None } else { Some(Utc::now()) }))
                    .execute(&mut txn)
                    .await?;

                Ok(Redirect::to(&format!(
                    "/{}/{}/discussion/{}",
                    tree.owner, tree.repo, tree.disc
                ))
                .into_response())
            })
            .scope_boxed()
        })
        .await
}

async fn can_add(
    config: &Config,
    txn: &mut AsyncPgConnection,
    repo_id: uuid::Uuid,
    disc_id: uuid::Uuid,
    hash: libpijul::Hash,
) -> Result<(), crate::Error> {
    use crate::db::discussion_changes::dsl as dc;
    let other_changes = dc::discussion_changes
        .filter(dc::discussion.eq(disc_id))
        .select((dc::change, dc::removed))
        .get_results::<(String, Option<DateTime<Utc>>)>(txn)
        .await?;
    let repo_locks = config.repo_locks.clone();
    let repo = repo_locks.get(&repo_id).await.unwrap();
    let change =
        tokio::task::spawn_blocking(move || repo.changes.get_change(&hash).unwrap()).await?;
    for (o, removed) in other_changes {
        if removed.is_some() {
            let o = libpijul::Hash::from_base32(o.as_bytes()).unwrap();
            if change.dependencies.contains(&o) {
                return Err(crate::Error::DependedUpon);
            }
        }
    }
    Ok(())
}

async fn can_remove(
    config: &Config,
    txn: &mut AsyncPgConnection,
    repo_id: uuid::Uuid,
    disc_id: uuid::Uuid,
    hash: libpijul::Hash,
) -> Result<(), crate::Error> {
    use crate::db::discussion_changes::dsl as dc;
    let other_changes = dc::discussion_changes
        .filter(dc::discussion.eq(disc_id))
        .select((dc::change, dc::removed))
        .get_results::<(String, Option<DateTime<Utc>>)>(txn)
        .await?;
    let repo_locks = config.repo_locks.clone();
    let repo = repo_locks.get(&repo_id).await.unwrap();
    tokio::task::spawn_blocking(move || {
        for (o, removed) in other_changes {
            if removed.is_none() {
                let o = libpijul::Hash::from_base32(o.as_bytes()).unwrap();
                let change = repo.changes.get_change(&o).unwrap();
                if change.dependencies.contains(&hash) {
                    return Err(crate::Error::DependedUpon);
                }
            }
        }
        Ok(())
    })
    .await?
}

#[derive(Debug, Deserialize)]
pub struct NewCommentForm {
    #[serde(default)]
    edit_comment: Option<uuid::Uuid>,
    new_discussion_comment: String,
    uniq: i64,
    token: String,
    #[serde(default)]
    action: Option<Action>,
}

#[debug_handler]
pub async fn discussion_post(
    State(config): State<Config>,
    jar: SignedCookieJar,
    token: axum_csrf::CsrfToken,
    accept_json: Option<TypedHeader<crate::config::AcceptJson>>,
    Path(tree): Path<DiscPath>,
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    Form(form): Form<NewCommentForm>,
) -> Result<Response, crate::Error> {
    debug!("discussion_post {:?}", form);

    token.verify(&form.token)?;

    debug!("new comment {:?}", tree);
    let (uid, login, email) = get_user_login_email_strict(&jar, &config).await?;
    let mut db = config.db.get().await?;
    let repo = crate::repository::repository(
        &mut db,
        &tree.owner,
        &tree.repo,
        uid,
        Perm::CREATE_DISCUSSION,
    )
    .await?;

    db.transaction(move |mut txn| {
        (async move {
            use crate::db::discussions::dsl as d;
            let ip_sql =
                ipnetwork::IpNetwork::new(addr.ip(), if addr.is_ipv4() { 32 } else { 128 })
                    .unwrap();
            let (disc_id, author): (uuid::Uuid, Option<uuid::Uuid>) = d::discussions
                .filter(d::repository_id.eq(repo.id))
                .filter(d::number.eq(tree.disc))
                .select((d::id, d::author))
                .get_result(&mut txn)
                .await
                .unwrap();

            match form.action {
                Some(Action::Close) | Some(Action::Reopen) => {
                    if author == Some(uid) || repo.permissions.contains(Perm::EDIT_DISCUSSION) {
                        use crate::db::discussion_tags::dsl as dt;
                        diesel::update(dt::discussion_tags)
                            .filter(dt::discussion.eq(disc_id))
                            .filter(dt::tag.is_null())
                            .set(dt::active.eq(false))
                            .execute(&mut txn)
                            .await?;

                        let n = diesel::insert_into(dt::discussion_tags)
                            .values((
                                dt::discussion.eq(disc_id),
                                dt::addition.eq(form.action == Some(Action::Reopen)),
                                dt::author.eq(uid),
                            ))
                            .execute(&mut txn)
                            .await?;

                        if n > 0 {
                            if let Some(Action::Close) = form.action {
                                diesel::update(d::discussions.find(disc_id))
                                    .set(d::closed.eq(diesel::dsl::now))
                                    .execute(&mut txn)
                                    .await?;
                            } else {
                                diesel::update(d::discussions.find(disc_id))
                                    .set(d::closed.eq(None::<chrono::DateTime<chrono::Utc>>))
                                    .execute(&mut txn)
                                    .await?;
                            }
                        }
                    }
                }
                _ => {}
            }

            use crate::db::comments::dsl as c;
            let comment = form.new_discussion_comment.trim();
            if !comment.is_empty() {
                let comment_html =
                    crate::markdown::render_markdown(&tree.owner, &tree.repo, comment);
                if let Some(cid) = form.edit_comment {
                    diesel::update(c::comments.find(cid))
                        .filter(c::author.eq(uid))
                        .set((
                            c::contents.eq(comment),
                            c::cached_html.eq(&comment_html.rendered),
                        ))
                        .execute(&mut txn)
                        .await?;
                } else {
                    diesel::insert_into(c::comments)
                        .values((
                            c::discussion_id.eq(disc_id),
                            c::author.eq(uid),
                            c::creation_ip.eq(&ip_sql),
                            c::contents.eq(comment),
                            c::cached_html.eq(&comment_html.rendered),
                            c::uniq.eq(&form.uniq),
                        ))
                        .execute(&mut txn)
                        .await?;
                }
            }

            use crate::db::discussion_subscriptions::dsl as ds;
            diesel::insert_into(ds::discussion_subscriptions)
                .values((ds::user_id.eq(uid), ds::discussion_id.eq(disc_id)))
                .execute(&mut txn)
                .await?;

            if let Some(TypedHeader(crate::config::AcceptJson(true))) = accept_json {
                Ok::<_, crate::Error>(
                    (
                        token.clone(),
                        Json(
                            discussion_json(
                                &config,
                                &mut txn,
                                repo.id,
                                token,
                                Path(tree),
                                Some(login),
                                Some(email),
                            )
                            .await?,
                        ),
                    )
                        .into_response(),
                )
            } else {
                Ok::<_, crate::Error>(
                    Redirect::to(&format!(
                        "/{}/{}/discussion/{}",
                        tree.owner, tree.repo, tree.disc
                    ))
                    .into_response(),
                )
            }
        })
        .scope_boxed()
    })
    .await
}

pub async fn discussion_changelist(
    db: &mut diesel_async::AsyncPgConnection,
    changes: &crate::repository::changestore::FileSystem,
    repo: uuid::Uuid,
    disc: i32,
) -> Result<Vec<u8>, crate::Error> {
    use crate::db::discussion_changes::dsl as dc;
    use crate::db::discussions::dsl as d;

    let mut n: usize = 0;
    let mut m = libpijul::pristine::Merkle::zero();
    let mut data = Vec::new();
    use libpijul::pristine::Base32;
    use std::io::Write;
    for h in dc::discussion_changes
        .inner_join(d::discussions)
        .filter(d::repository_id.eq(repo))
        .filter(d::number.eq(disc))
        .filter(dc::removed.is_null())
        .order_by(dc::added)
        .select(dc::change)
        .get_results::<String>(db)
        .await?
    {
        let hh = libpijul::pristine::Hash::from_base32(h.as_bytes()).unwrap();
        if changes.has_change(&hh) {
            m = m.next(&hh);
            writeln!(data, "{}.{}.{}", n, h, m.to_base32())?;
            n += 1
        }
    }
    data.push(b'\n');
    Ok(data)
}

pub async fn discussion_state(
    db: &mut diesel_async::AsyncPgConnection,
    repo: uuid::Uuid,
    disc: i32,
    at: Option<u64>,
) -> Result<(usize, libpijul::pristine::Merkle), crate::Error> {
    use crate::db::discussion_changes::dsl as dc;
    use crate::db::discussions::dsl as d;

    let q = dc::discussion_changes
        .inner_join(d::discussions)
        .filter(d::repository_id.eq(repo))
        .filter(d::number.eq(disc))
        .filter(dc::removed.is_null())
        .order_by(dc::added)
        .select(dc::change);

    let it = if let Some(at) = at {
        q.limit(at as i64 + 1).get_results::<String>(db).await?
        /*
            self.query_raw(
                "SELECT change FROM discussion_changes JOIN discussions ON discussion_changes.discussion = discussions.id WHERE discussions.repository_id = $1 AND discussions.number = $2 AND removed IS NULL ORDER BY added ASC LIMIT $3",
                crate::helpers::slice_iter(&[&repo, &disc, &(at as i64 + 1)])
        ).await?
            */
    } else {
        q.get_results::<String>(db).await?
        /*
            self.query_raw(
                "SELECT change FROM discussion_changes JOIN discussions ON discussion_changes.discussion = discussions.id WHERE discussions.repository_id = $1 AND discussions.number = $2 AND removed IS NULL ORDER BY added ASC",
                crate::helpers::slice_iter(&[&repo, &disc])
        ).await?
            */
    };
    let mut n = 0;
    let mut m = libpijul::pristine::Merkle::zero();
    use libpijul::pristine::Base32;
    for h in it {
        let hh = libpijul::pristine::Hash::from_base32(h.as_bytes()).unwrap();
        m = m.next(&hh);
        n += 1
    }
    Ok((n, m))
}