use std::sync::Arc;

use actix_web::{HttpRequest, HttpResponse, get, post, web};
use sea_orm::ActiveValue::Set;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, PaginatorTrait,
    QueryFilter,
};
use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;

use crate::auth::{CurrentUser, viewer_name};
use crate::entities::prelude::*;
use crate::entities::users;
use crate::views::profile::ActorProfile;
use crate::webui::repositories;
use crate::{AppState, state_read, views};

/// What the profile editing form posts.
#[derive(Debug, Deserialize)]
pub struct EditProfile {
    description: String,
}

#[tracing::instrument(level = "debug", skip(db))]
async fn get_user(db: &DatabaseConnection, name: &str) -> Result<Option<users::Model>, DbErr> {
    Users::find()
        .filter(users::Column::Name.eq(name))
        .one(db)
        .await
}

/// Profiles live under a `~`, the way `example.org/~user` has worked since
/// before the web was a platform. It also keeps `/~name` from colliding with
/// whatever single-segment routes come later - repositories, settings, sign
/// in - without every one of them having to dodge `/{name}`.
#[tracing::instrument(skip_all, fields(user_or_team = %user_or_team.as_str()))]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn get_profile(
    data: web::Data<Arc<RwLock<AppState>>>,
    viewer: Option<CurrentUser>,
    user_or_team: web::Path<String>,
    req: HttpRequest,
) -> HttpResponse {
    // Read the user out of the database if there is one.
    let state = state_read(&data).await;
    let user = match get_user(&state.db, user_or_team.as_str()).await {
        Ok(user) => user,
        Err(error) => {
            error!(%error, user = %user_or_team.as_str(), "looking up the user failed");
            return HttpResponse::InternalServerError().finish();
        }
    };
    let Some(user) = user else {
        drop(state);
        return HttpResponse::NotFound().body(format!("User `{user_or_team}` could not be found"));
    };

    let repositories = match repositories::summaries(
        &state.db,
        user.id,
        &user.name,
        viewer.as_ref().map(|viewer| viewer.id),
    )
    .await
    {
        Ok(repositories) => repositories,
        Err(error) => {
            error!(%error, user = %user.name, "listing repositories failed");
            return HttpResponse::InternalServerError().finish();
        }
    };

    // Load notes if federation is enabled
    let note_views = if crate::activitypub::enabled(&state) {
        // Get the actor for this user
        let actor = match crate::activitypub::actor::get_or_create(&state.db, user.id).await {
            Ok(actor) => actor,
            Err(error) => {
                error!(%error, user = %user.name, "getting actor failed");
                return HttpResponse::InternalServerError().finish();
            }
        };

        // Load notes for this actor
        let notes = match crate::activitypub::notes::list(&state.db, actor.id).await {
            Ok(notes) => notes,
            Err(error) => {
                error!(%error, user = %user.name, "listing notes failed");
                return HttpResponse::InternalServerError().finish();
            }
        };

        // Load reactions for each note
        let mut note_view_list = Vec::new();

        // Query viewer's reactions to these notes (if viewer exists)
        let viewer_reacted_note_ids = if let Some(viewer_user) = &viewer {
            let note_ids: Vec<i64> = notes.iter().map(|n| n.id).collect();
            if !note_ids.is_empty() {
                match crate::entities::prelude::ApReactions::find()
                    .filter(crate::entities::ap_reactions::Column::NoteId.is_in(note_ids))
                    .filter(
                        crate::entities::ap_reactions::Column::LocalUserId.eq(Some(viewer_user.id)),
                    )
                    .filter(crate::entities::ap_reactions::Column::Kind.eq("like"))
                    .all(&state.db)
                    .await
                {
                    Ok(reactions) => reactions.iter().filter_map(|r| r.note_id).collect(),
                    Err(error) => {
                        error!(%error, "querying viewer reactions failed");
                        Vec::new()
                    }
                }
            } else {
                Vec::new()
            }
        } else {
            Vec::new()
        };

        // Query viewer's boosted notes
        let viewer_boosted_note_ids = if let Some(viewer_user) = &viewer {
            let note_ids: Vec<i64> = notes.iter().map(|n| n.id).collect();
            if !note_ids.is_empty() {
                match crate::entities::prelude::ApReactions::find()
                    .filter(crate::entities::ap_reactions::Column::NoteId.is_in(note_ids))
                    .filter(
                        crate::entities::ap_reactions::Column::LocalUserId.eq(Some(viewer_user.id)),
                    )
                    .filter(crate::entities::ap_reactions::Column::Kind.eq("announce"))
                    .all(&state.db)
                    .await
                {
                    Ok(reactions) => reactions.iter().filter_map(|r| r.note_id).collect(),
                    Err(error) => {
                        error!(%error, "querying viewer boosts failed");
                        Vec::new()
                    }
                }
            } else {
                Vec::new()
            }
        } else {
            Vec::new()
        };

        let public_url = state.public_url().to_string();
        let viewer_username = viewer.as_ref().map(|v| v.name.clone());

        for note in notes {
            match crate::activitypub::notes::get_reactions(&state.db, note.id).await {
                Ok(reactions) => {
                    let viewer_reacted = viewer_reacted_note_ids.contains(&note.id);
                    let viewer_boosted = viewer_boosted_note_ids.contains(&note.id);
                    note_view_list.push(views::NoteView {
                        actor_profile: ActorProfile {
                            display_name: user.name.clone(),
                            handle: user.name.clone(),
                            host: None,
                            profile_url: None,
                            icon_url: None, // Local users don't have avatars yet
                            actor_url: format!("/~{}", user.name),
                        },
                        note,
                        reactions,
                        viewer_reacted,
                        viewer_boosted,
                        public_url: public_url.clone(),
                        viewer_username: viewer_username.clone(),
                    });
                }
                Err(error) => {
                    error!(%error, note_id = note.id, "loading reactions failed");
                    // Continue without reactions for this note
                }
            }
        }
        note_view_list
    } else {
        vec![]
    };

    // Load follower count if federation is enabled
    let follower_count = if crate::activitypub::enabled(&state) {
        match crate::activitypub::actor::get_or_create(&state.db, user.id).await {
            Ok(actor) => {
                match crate::entities::prelude::ApFollowers::find()
                    .filter(crate::entities::ap_followers::Column::LocalActorId.eq(actor.id))
                    .count(&state.db)
                    .await
                {
                    Ok(count) => count,
                    Err(error) => {
                        error!(%error, user = %user.name, "counting followers failed");
                        0
                    }
                }
            }
            Err(error) => {
                error!(%error, user = %user.name, "getting actor failed");
                0
            }
        }
    } else {
        0
    };

    // Build fediverse handle and actor URL if federation is enabled
    let (handle, actor_url) = if crate::activitypub::enabled(&state) {
        let urls = crate::activitypub::ApUrls::new(state.public_url());
        let handle = Some(format!("@{}@{}", user.name, urls.host()));
        let actor_url = Some(urls.actor(&user.name));
        (handle, actor_url)
    } else {
        (None, None)
    };

    // Load following list if viewer is the owner and federation is enabled
    let is_owner = viewer.as_ref().map(|v| v.name.as_str()) == Some(user.name.as_str());
    let federation_enabled = crate::activitypub::enabled(&state);
    let following = if is_owner && federation_enabled {
        match crate::activitypub::actor::get_or_create(&state.db, user.id).await {
            Ok(actor) => {
                use crate::entities::ap_following;

                match crate::entities::prelude::ApFollowing::find()
                    .filter(ap_following::Column::LocalActorId.eq(actor.id))
                    .find_also_related(ApRemoteActors)
                    .all(&state.db)
                    .await
                {
                    Ok(rows) => {
                        rows.into_iter()
                            .filter_map(|(following, remote)| {
                                remote.map(|r| {
                                    let display_name =
                                        r.preferred_username.clone().unwrap_or_else(|| {
                                            // Extract username from actor_url
                                            r.actor_url
                                                .split('/')
                                                .next_back()
                                                .unwrap_or("unknown")
                                                .to_string()
                                        });
                                    views::FollowingEntry {
                                        actor_url: r.actor_url.clone(),
                                        display_name,
                                        state: following.state.clone(),
                                    }
                                })
                            })
                            .collect()
                    }
                    Err(error) => {
                        error!(%error, user = %user.name, "loading following failed");
                        vec![]
                    }
                }
            }
            Err(error) => {
                error!(%error, user = %user.name, "getting actor failed");
                vec![]
            }
        }
    } else {
        vec![]
    };

    // Extract error/notice messages from query parameters
    let query_string = req.query_string();
    let error_message = query_string
        .split('&')
        .find(|pair| pair.starts_with("error="))
        .and_then(|pair| pair.strip_prefix("error="))
        .map(|s| {
            url::form_urlencoded::parse(s.as_bytes())
                .map(|(k, _)| k)
                .collect::<String>()
        });

    let notice_message = query_string
        .split('&')
        .find(|pair| pair.starts_with("notice="))
        .and_then(|pair| pair.strip_prefix("notice="))
        .map(|s| {
            url::form_urlencoded::parse(s.as_bytes())
                .map(|(k, _)| k)
                .collect::<String>()
        });

    // Let go of the read lock before rendering.
    drop(state);

    views::profile(
        viewer_name(viewer.as_ref()),
        &user.name,
        user.description.as_deref(),
        handle,
        actor_url,
        follower_count,
        federation_enabled,
        following,
        error_message,
        notice_message,
        &repositories,
        &note_views,
    )
}

/// The form for editing one's own profile. [`CurrentUser`] turns an anonymous
/// request into a `401` before this runs; a session for somebody else is a
/// `403`, since there is no reason a signed-in user should be able to probe
/// another one's editor.
#[get("/~{user_or_team}/edit")]
#[tracing::instrument(skip_all, fields(user_or_team = %user_or_team.as_str()))]
async fn get_edit_profile(
    data: web::Data<Arc<RwLock<AppState>>>,
    current: CurrentUser,
    user_or_team: web::Path<String>,
) -> HttpResponse {
    let state = state_read(&data).await;
    let user = match get_user(&state.db, user_or_team.as_str()).await {
        Ok(user) => user,
        Err(error) => {
            error!(%error, user = %user_or_team.as_str(), "looking up the user failed");
            return HttpResponse::InternalServerError().finish();
        }
    };
    drop(state);

    let Some(user) = user else {
        return HttpResponse::NotFound().body(format!("User `{user_or_team}` could not be found"));
    };

    if user.id != current.id {
        return HttpResponse::Forbidden().finish();
    }

    views::edit_profile(Some(&current.name), &user.name, user.description.as_deref())
}

#[post("/~{user_or_team}/edit")]
#[tracing::instrument(skip_all, fields(user_or_team = %user_or_team.as_str()))]
async fn post_edit_profile(
    data: web::Data<Arc<RwLock<AppState>>>,
    current: CurrentUser,
    user_or_team: web::Path<String>,
    form: web::Form<EditProfile>,
) -> HttpResponse {
    let state = state_read(&data).await;
    let user = match get_user(&state.db, user_or_team.as_str()).await {
        Ok(user) => user,
        Err(error) => {
            error!(%error, user = %user_or_team.as_str(), "looking up the user failed");
            return HttpResponse::InternalServerError().finish();
        }
    };
    drop(state);

    let Some(user) = user else {
        return HttpResponse::NotFound().body(format!("User `{user_or_team}` could not be found"));
    };

    if user.id != current.id {
        return HttpResponse::Forbidden().finish();
    }

    let name = user.name.clone();
    let mut edited: users::ActiveModel = user.into();
    edited.description = Set(Some(form.into_inner().description));

    let state = state_read(&data).await;
    if let Err(error) = edited.update(&state.db).await {
        error!(%error, user = %name, "updating the profile failed");
        return HttpResponse::InternalServerError().finish();
    }
    drop(state);

    HttpResponse::SeeOther()
        .append_header(("Location", format!("/~{name}")))
        .finish()
}
