use std::sync::Arc;

use actix_web::{HttpResponse, get, web};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tokio::sync::RwLock;

use crate::AppState;
use crate::auth::CurrentUser;
use crate::entities::prelude::*;
use crate::entities::users::Column as UserColumn;
use crate::views;

pub mod accounts;
pub mod federation;
pub mod git;
pub mod invites;
pub mod nodeinfo;
pub mod remote_forge;
pub mod repositories;
pub mod ssh_keys;
pub mod user;

// TODO: Split this? :D
pub(crate) fn configure(cfg: &mut web::ServiceConfig) {
    cfg.service(index)
        .service(accounts::get_register)
        .service(accounts::post_register)
        .service(accounts::get_login)
        .service(accounts::post_login)
        .service(accounts::post_logout)
        .service(federation::get_webfinger)
        .service(nodeinfo::get_nodeinfo_jdd)
        .service(nodeinfo::get_nodeinfo_21)
        .service(
            web::resource("/~{user_or_team}")
                .route(
                    web::get()
                        .guard(federation::accept_ap())
                        .to(federation::get_actor),
                )
                .route(web::get().to(user::get_profile)),
        )
        .service(
            web::resource("/~{user_or_team}/inbox")
                .app_data(web::PayloadConfig::default().limit(1_048_576))
                .route(web::post().to(federation::post_inbox)),
        )
        .service(federation::get_followers)
        .service(federation::get_outbox)
        .service(federation::get_note)
        .service(federation::post_note)
        .service(federation::post_react)
        .service(federation::post_unreact)
        .service(federation::post_react_remote)
        .service(federation::post_unreact_remote)
        .service(federation::post_announce_remote)
        .service(federation::post_unannounce_remote)
        .service(federation::post_follow)
        .service(federation::post_unfollow)
        .service(user::get_edit_profile)
        .service(user::post_edit_profile)
        .service(invites::get_invites)
        .service(invites::post_invite)
        .service(repositories::get_new_repository)
        .service(repositories::post_new_repository)
        .service(ssh_keys::get_ssh_keys)
        .service(ssh_keys::post_ssh_key)
        .service(ssh_keys::post_delete_ssh_key)
        .service(remote_forge::get_remote_tickets)
        .service(remote_forge::get_new_remote_ticket)
        .service(remote_forge::post_remote_ticket)
        // Git over HTTP, read-only. Its paths end in a literal `info/refs` or
        // `git-upload-pack`, which no repository page of ours is called, so
        // they cannot be reached by mistake; the `.git` in the URL is what
        // `RepositoryPath::parse` requires before it names a repository at all.
        .service(git::info_refs)
        .service(git::upload_pack)
        .service(git::receive_pack)
        // The refs, tree and file browsers sit one segment deeper -
        // `/{repository_slug}/refs`, `/{repository_slug}/tree`,
        // `/{repository_slug}/raw` - so the repository page's own pattern
        // can never reach them, and no word needs reserving as a slug:
        // `/~{user}/refs` is still a repository, and so are `/~{user}/tree`
        // and `/~{user}/raw`.
        .service(repositories::get_refs)
        .service(repositories::get_commit)
        .service(repositories::get_commits)
        .service(repositories::get_commits_root)
        .service(repositories::get_commits_root_slash)
        .service(repositories::get_compare)
        .service(repositories::get_compare_root)
        .service(repositories::get_compare_root_slash)
        .service(repositories::get_tree)
        .service(repositories::get_tree_revision)
        .service(repositories::get_tree_root)
        .service(repositories::get_tree_root_slash)
        .service(repositories::get_raw)
        .service(repositories::get_raw_revision)
        // Tickets are two segments deeper than the repository page, like
        // the tree browsers above, and the literal `new` address has to be
        // tried before the one that takes any number as a ticket's own.
        .service(repositories::tickets::get_tickets)
        .service(repositories::tickets::get_new_ticket)
        .service(repositories::tickets::post_ticket)
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/tickets/{number}")
                .route(
                    web::get()
                        .guard(federation::accept_ap())
                        .to(federation::get_ticket_ap),
                )
                .route(web::get().to(repositories::tickets::get_ticket))
                .route(web::post().to(repositories::tickets::post_edit_ticket)),
        )
        .service(repositories::tickets::get_edit_ticket)
        .service(repositories::tickets::post_delete_ticket)
        .service(repositories::tickets::post_attachment)
        .service(repositories::tickets::get_attachment)
        .service(repositories::tickets::post_delete_attachment)
        .service(repositories::tickets::post_comment)
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/tickets/{number}/comments/{uuid}")
                .route(
                    web::get()
                        .guard(federation::accept_ap())
                        .to(federation::get_ticket_comment_ap),
                )
                .route(web::post().to(repositories::tickets::post_edit_comment)),
        )
        .service(repositories::tickets::post_delete_comment)
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/tickets/{number}/replies")
                .route(web::get().to(federation::get_ticket_replies)),
        )
        // Patches are two segments deeper than the repository page, like
        // tickets, and follow the same pattern: list, detail, edit.
        .service(repositories::patches::get_patches)
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/patches/{number}")
                .route(
                    web::get()
                        .guard(federation::accept_ap())
                        .to(federation::get_patch_ap),
                )
                .route(web::get().to(repositories::patches::get_patch))
                .route(web::post().to(repositories::patches::post_edit_patch)),
        )
        .service(repositories::patches::get_edit_patch)
        .service(repositories::patches::post_merge_patch)
        .service(repositories::patches::post_comment)
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/patches/{number}/comments/{uuid}")
                .route(
                    web::get()
                        .guard(federation::accept_ap())
                        .to(federation::get_patch_comment_ap),
                )
                .route(web::post().to(repositories::patches::post_edit_comment)),
        )
        .service(repositories::patches::post_delete_comment)
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/patches/{number}/replies")
                .route(web::get().to(federation::get_patch_replies)),
        )
        // Settings: visibility and label management. Owner-only, so the
        // handlers refuse non-owners with 403 and anonymous visitors with 401.
        .service(repositories::settings::get_settings)
        .service(repositories::settings::post_settings)
        .service(repositories::settings::post_label)
        .service(repositories::settings::post_delete_label)
        // Repository ActivityPub endpoints: inbox, followers, outbox, commits
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/inbox")
                .app_data(web::PayloadConfig::default().limit(1_048_576))
                .route(web::post().to(federation::post_repo_inbox)),
        )
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/followers")
                .route(web::get().to(federation::get_repo_followers)),
        )
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/outbox")
                .route(web::get().to(federation::get_repo_outbox)),
        )
        .service(
            web::resource("/~{user_or_team}/{repository_slug}/commits/{hash}")
                .route(web::get().to(federation::get_repo_commit)),
        )
        // Registered last: `/~{user}/{repository_slug}` is the most general
        // pattern under a profile, and every literal suffix above has to be
        // tried first. Each literal suffix (`edit`, `ssh-keys`,
        // `repositories`, ...) is a name a repository may not take; they are
        // listed in `repositories::RESERVED_SLUGS`, which has to be kept in
        // step with this list.
        // Now with content negotiation: ActivityPub actor document for AP clients,
        // HTML repository landing page for browsers.
        .service(
            web::resource("/~{user_or_team}/{repository_slug}")
                .route(
                    web::get()
                        .guard(federation::accept_ap())
                        .to(federation::get_repo_actor),
                )
                .route(web::get().to(repositories::get_repository)),
        );
}

#[derive(serde::Deserialize)]
struct IndexQuery {
    page: Option<u64>,
}

#[get("/")]
#[tracing::instrument(skip_all)]
pub async fn index(
    viewer: Option<CurrentUser>,
    data: web::Data<Arc<RwLock<AppState>>>,
    query: web::Query<IndexQuery>,
) -> HttpResponse {
    let viewer_name = crate::auth::viewer_name(viewer.as_ref());
    let state = data.read().await;
    let db = state.db.clone();
    let public_url = state.public_url().to_string();

    // Parse page number, default to 0, invalid -> 0
    let page = query.page.unwrap_or(0);

    // Only show feed if logged in and federation is enabled
    if viewer_name.is_none() || !crate::activitypub::enabled(&state) {
        return views::index(viewer_name, vec![], &public_url, page);
    }

    // Get the user ID from the viewer
    let viewer_name_str = viewer_name.as_ref().unwrap();
    let user = match Users::find()
        .filter(UserColumn::Name.eq(*viewer_name_str))
        .one(&db)
        .await
    {
        Ok(Some(u)) => u,
        _ => return views::index(viewer_name, vec![], &public_url, page),
    };

    // Fetch the feed
    let entries = match crate::activitypub::feed::get_home_feed(
        &db,
        user.id,
        &public_url,
        crate::views::index::FEED_PAGE_SIZE as u64,
        page,
    )
    .await
    {
        Ok(entries) => entries,
        Err(e) => {
            tracing::warn!(error = %e, "failed to fetch home feed");
            vec![]
        }
    };

    views::index(viewer_name, entries, &public_url, page)
}
