use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use rand::Rng;
use std::sync::Arc;
use uuid::Uuid;

use crate::{
    middleware::auth::AuthUser,
    models::{CreateInviteRequest, NostrInviteRequest},
    AppState,
};

/// Response payload for a newly created invite link.
#[derive(serde::Serialize)]
pub struct InviteResponse {
    pub id: String,
    pub token: String,
    pub invite_url: String,
}

/// Create a one-time invite token for a room member to share.
pub async fn create_invite(
    State(state): State<Arc<AppState>>,
    auth: AuthUser,
    Json(body): Json<CreateInviteRequest>,
) -> Result<Json<InviteResponse>, (StatusCode, String)> {
    // Verify sender is a member
    let is_member = sqlx::query_scalar::<_, String>(
        "SELECT user_id FROM room_members WHERE room_id = ? AND user_id = ?",
    )
    .bind(&body.room_id)
    .bind(&auth.user_id)
    .fetch_optional(&state.db)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    if is_member.is_none() {
        return Err((StatusCode::FORBIDDEN, "Not a member of this room".into()));
    }

    let invite_id = Uuid::new_v4().to_string();
    let token: String = rand::thread_rng()
        .sample_iter(&rand::distributions::Alphanumeric)
        .take(32)
        .map(char::from)
        .collect();

    sqlx::query(
        "INSERT INTO invites (id, room_id, invited_by, email, token) VALUES (?, ?, ?, ?, ?)",
    )
    .bind(&invite_id)
    .bind(&body.room_id)
    .bind(&auth.user_id)
    .bind(&body.email)
    .bind(&token)
    .execute(&state.db)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    Ok(Json(InviteResponse {
        id: invite_id,
        token: token.clone(),
        invite_url: format!("/invite/{}", token),
    }))
}

/// Response payload returned after consuming an invite.
#[derive(serde::Serialize)]
pub struct AcceptInviteResponse {
    pub room_id: String,
}

/// Consume an invite token and add the caller to the room.
pub async fn accept_invite(
    State(state): State<Arc<AppState>>,
    auth: AuthUser,
    Path(token): Path<String>,
) -> Result<Json<AcceptInviteResponse>, (StatusCode, String)> {
    let invite = sqlx::query_as::<_, (String, String, bool)>(
        "SELECT id, room_id, used FROM invites WHERE token = ?",
    )
    .bind(&token)
    .fetch_optional(&state.db)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
    .ok_or((StatusCode::NOT_FOUND, "Invite not found".into()))?;

    let (invite_id, room_id, used) = invite;

    if used {
        return Err((StatusCode::GONE, "Invite already used".into()));
    }

    // Verify room is not deleted
    let room_active =
        sqlx::query_scalar::<_, String>("SELECT id FROM rooms WHERE id = ? AND deleted_at IS NULL")
            .bind(&room_id)
            .fetch_optional(&state.db)
            .await
            .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    if room_active.is_none() {
        return Err((StatusCode::GONE, "This room has been deleted".into()));
    }

    // Mark invite as used
    sqlx::query("UPDATE invites SET used = 1 WHERE id = ?")
        .bind(&invite_id)
        .execute(&state.db)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    // Add user to room
    sqlx::query("INSERT OR IGNORE INTO room_members (room_id, user_id) VALUES (?, ?)")
        .bind(&room_id)
        .bind(&auth.user_id)
        .execute(&state.db)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    Ok(Json(AcceptInviteResponse { room_id }))
}

/// Result of a Nostr-based room invite attempt.
#[derive(serde::Serialize)]
pub struct NostrInviteResponse {
    pub status: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
}

/// Add a user to a room by their Nostr public key if they already have an account.
pub async fn invite_by_nostr(
    State(state): State<Arc<AppState>>,
    auth: AuthUser,
    Json(body): Json<NostrInviteRequest>,
) -> Result<Json<NostrInviteResponse>, (StatusCode, String)> {
    // Normalize pubkey: if it starts with "npub", decode bech32
    let pubkey_hex = if body.nostr_pubkey.starts_with("npub") {
        nostr::prelude::PublicKey::parse(&body.nostr_pubkey)
            .map(|pk| pk.to_hex())
            .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid npub format".to_string()))?
    } else {
        // Validate it's valid hex
        if body.nostr_pubkey.len() != 64
            || !body.nostr_pubkey.chars().all(|c| c.is_ascii_hexdigit())
        {
            return Err((
                StatusCode::BAD_REQUEST,
                "Invalid pubkey: must be 64-char hex or npub".to_string(),
            ));
        }
        body.nostr_pubkey.clone()
    };

    // Verify inviter is a member of the room
    let is_member = sqlx::query_scalar::<_, String>(
        "SELECT user_id FROM room_members WHERE room_id = ? AND user_id = ?",
    )
    .bind(&body.room_id)
    .bind(&auth.user_id)
    .fetch_optional(&state.db)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    if is_member.is_none() {
        return Err((StatusCode::FORBIDDEN, "Not a member of this room".into()));
    }

    // Lookup user by nostr_pubkey
    let target_user = sqlx::query_as::<_, (String, String)>(
        "SELECT id, display_name FROM users WHERE nostr_pubkey = ?",
    )
    .bind(&pubkey_hex)
    .fetch_optional(&state.db)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    match target_user {
        Some((user_id, display_name)) => {
            // Add to room
            sqlx::query("INSERT OR IGNORE INTO room_members (room_id, user_id) VALUES (?, ?)")
                .bind(&body.room_id)
                .bind(&user_id)
                .execute(&state.db)
                .await
                .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

            Ok(Json(NostrInviteResponse {
                status: "added".to_string(),
                display_name: Some(display_name),
            }))
        }
        None => Ok(Json(NostrInviteResponse {
            status: "not_found".to_string(),
            display_name: None,
        })),
    }
}
