use axum::{
    Json, Router,
    extract::{Path, Query, State, WebSocketUpgrade},
    http::{HeaderMap, StatusCode, header},
    response::{Html, IntoResponse, Redirect},
    routing::{get, post},
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{
    AppState,
    auth::{self, ApiError, AuthUser},
    db, voice,
};

pub fn routes() -> Router<AppState> {
    Router::new()
        .route("/", get(index))
        .route("/auth/login", get(auth_login))
        .route("/auth/callback", get(auth_callback))
        .route("/auth/logout", post(auth_logout))
        .route("/me", get(me))
        .route("/dms", get(list_dm_conversations))
        .route("/dms/{other_user_id}/messages", get(list_dm_messages).post(send_dm_message))
        .route("/rtc-config", get(rtc_config))
        .route("/guilds", get(list_guilds).post(create_guild))
        .route("/guilds/{guild_id}/members", get(list_guild_members))
        .route("/guilds/{guild_id}/channels", get(list_channels))
        .route("/guilds/{guild_id}/voice-presence", get(guild_voice_presence))
        .route("/guilds/{guild_id}/invites", post(create_invite))
        .route("/invites/{code}/join", post(join_invite))
        .route("/channels", post(create_channel))
        .route("/channels/{channel_id}/messages", get(list_messages).post(send_message))
        .route("/channels/{channel_id}/voice/ws", get(voice_ws))
}

pub async fn health() -> &'static str {
    "ok"
}

async fn index() -> Html<&'static str> {
    Html(include_str!("../static/index.html"))
}

async fn auth_login(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
    let oauth_state = auth::new_oauth_state();

    let authorize_url = format!(
        "{}?response_type=code&client_id={}&redirect_uri={}&scope={}&state={}",
        state.settings.oidc_authorize_url,
        urlencoding::encode(&state.settings.oidc_client_id),
        urlencoding::encode(&state.settings.oidc_redirect_url),
        urlencoding::encode(&state.settings.oidc_scopes),
        urlencoding::encode(&oauth_state)
    );

    let mut headers = HeaderMap::new();
    headers.insert(
        header::SET_COOKIE,
        auth::make_oauth_state_cookie(&oauth_state, state.settings.cookie_secure)
            .parse()
            .map_err(|_| ApiError::internal("failed to set oauth cookie"))?,
    );

    Ok((headers, Redirect::to(&authorize_url)))
}

#[derive(Deserialize)]
struct AuthCallbackQuery {
    code: String,
    state: String,
}

#[derive(Deserialize)]
struct OidcTokenResponse {
    access_token: String,
}

#[derive(Deserialize)]
struct OidcUserInfo {
    sub: String,
    email: Option<String>,
    preferred_username: Option<String>,
    nickname: Option<String>,
    given_name: Option<String>,
    family_name: Option<String>,
    name: Option<String>,
    picture: Option<String>,
}

fn normalized_claim(value: Option<&str>) -> Option<String> {
    let v = value?.trim();
    if v.is_empty() || v.eq_ignore_ascii_case("null") {
        None
    } else {
        Some(v.to_string())
    }
}

fn choose_display_name(profile: &OidcUserInfo) -> String {
    if let Some(v) = normalized_claim(profile.name.as_deref()) {
        return v;
    }
    if let Some(v) = normalized_claim(profile.nickname.as_deref()) {
        return v;
    }
    if let Some(v) = normalized_claim(profile.preferred_username.as_deref()) {
        return v;
    }

    let given = normalized_claim(profile.given_name.as_deref());
    let family = normalized_claim(profile.family_name.as_deref());
    match (given, family) {
        (Some(g), Some(f)) => format!("{g} {f}"),
        (Some(g), None) => g,
        (None, Some(f)) => f,
        (None, None) => "User".to_string(),
    }
}

async fn auth_callback(
    State(state): State<AppState>,
    Query(query): Query<AuthCallbackQuery>,
    headers: HeaderMap,
) -> Result<impl IntoResponse, ApiError> {
    auth::validate_oauth_state(auth::read_oauth_state_from_headers(&headers), &query.state)
        .map_err(|e| ApiError::bad_request(&e.to_string()))?;

    let token_res = state
        .http
        .post(&state.settings.oidc_token_url)
        .form(&[
            ("grant_type", "authorization_code"),
            ("code", query.code.as_str()),
            ("redirect_uri", state.settings.oidc_redirect_url.as_str()),
            ("client_id", state.settings.oidc_client_id.as_str()),
            ("client_secret", state.settings.oidc_client_secret.as_str()),
        ])
        .send()
        .await
        .map_err(|e| ApiError::bad_request(&format!("token exchange failed: {e}")))?;

    if !token_res.status().is_success() {
        let text = token_res.text().await.unwrap_or_else(|_| "<no body>".to_string());
        return Err(ApiError::bad_request(&format!(
            "token exchange returned non-success: {text}"
        )));
    }

    let token: OidcTokenResponse = token_res
        .json()
        .await
        .map_err(|e| ApiError::bad_request(&format!("invalid token response: {e}")))?;

    let userinfo_res = state
        .http
        .get(&state.settings.oidc_userinfo_url)
        .bearer_auth(&token.access_token)
        .send()
        .await
        .map_err(|e| ApiError::bad_request(&format!("userinfo request failed: {e}")))?;

    if !userinfo_res.status().is_success() {
        let text = userinfo_res.text().await.unwrap_or_else(|_| "<no body>".to_string());
        return Err(ApiError::bad_request(&format!(
            "userinfo returned non-success: {text}"
        )));
    }

    let profile: OidcUserInfo = userinfo_res
        .json()
        .await
        .map_err(|e| ApiError::bad_request(&format!("invalid userinfo response: {e}")))?;

    let display_name = choose_display_name(&profile);

    let user = db::upsert_user_from_oidc(
        &state.db,
        &profile.sub,
        profile.email.as_deref(),
        &display_name,
        profile.picture.as_deref(),
    )
    .await
    .map_err(|e| ApiError::internal(&format!("failed to persist user: {e}")))?;

    let session_cookie =
        auth::new_session_cookie(user.id, &state.settings.session_secret, state.settings.cookie_secure)
            .map_err(|e| ApiError::internal(&e.to_string()))?;

    let mut headers = HeaderMap::new();
    headers.append(
        header::SET_COOKIE,
        session_cookie
            .parse()
            .map_err(|_| ApiError::internal("failed to set session cookie"))?,
    );
    headers.append(
        header::SET_COOKIE,
        auth::clear_oauth_state_cookie(state.settings.cookie_secure)
            .parse()
            .map_err(|_| ApiError::internal("failed to clear oauth state cookie"))?,
    );

    Ok((headers, Redirect::to("/")))
}

async fn auth_logout(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
    let mut headers = HeaderMap::new();
    headers.append(
        header::SET_COOKIE,
        auth::clear_session_cookie(state.settings.cookie_secure)
            .parse()
            .map_err(|_| ApiError::internal("failed to clear session cookie"))?,
    );

    Ok((StatusCode::NO_CONTENT, headers))
}

#[derive(Serialize)]
struct RtcConfigResponse {
    ice_servers: Vec<RtcIceServer>,
}

#[derive(Serialize)]
struct RtcIceServer {
    urls: Vec<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    username: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    credential: Option<String>,
}

async fn rtc_config(State(state): State<AppState>, _user: AuthUser) -> Result<impl IntoResponse, ApiError> {
    let mut ice_servers = Vec::new();

    if !state.settings.stun_urls.is_empty() {
        ice_servers.push(RtcIceServer {
            urls: state.settings.stun_urls.clone(),
            username: None,
            credential: None,
        });
    }

    if !state.settings.turn_urls.is_empty() {
        ice_servers.push(RtcIceServer {
            urls: state.settings.turn_urls.clone(),
            username: state.settings.turn_username.clone(),
            credential: state.settings.turn_password.clone(),
        });
    }

    if ice_servers.is_empty() {
        ice_servers.push(RtcIceServer {
            urls: vec!["stun:stun.l.google.com:19302".to_string()],
            username: None,
            credential: None,
        });
    }

    Ok(Json(RtcConfigResponse { ice_servers }))
}

async fn me(State(state): State<AppState>, user: AuthUser) -> Result<impl IntoResponse, ApiError> {
    let me = db::get_user_by_id(&state.db, user.id)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to load user: {e}")))?
        .ok_or_else(|| ApiError::unauthorized("no user"))?;

    Ok(Json(me))
}

async fn list_dm_conversations(
    State(state): State<AppState>,
    user: AuthUser,
) -> Result<impl IntoResponse, ApiError> {
    let conversations = db::list_dm_conversations(&state.db, user.id)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to list dm conversations: {e}")))?;
    Ok(Json(conversations))
}

async fn list_guild_members(
    State(state): State<AppState>,
    user: AuthUser,
    Path(guild_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_guild_member(&state, guild_id, user.id).await?;
    let members = db::list_guild_members(&state.db, guild_id)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to list guild members: {e}")))?;
    Ok(Json(members))
}

async fn list_guilds(
    State(state): State<AppState>,
    user: AuthUser,
) -> Result<impl IntoResponse, ApiError> {
    let guilds = db::list_guilds_for_user(&state.db, user.id)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to list guilds: {e}")))?;

    Ok(Json(guilds))
}

#[derive(Deserialize)]
struct CreateGuildBody {
    name: String,
}

async fn create_guild(
    State(state): State<AppState>,
    user: AuthUser,
    Json(body): Json<CreateGuildBody>,
) -> Result<impl IntoResponse, ApiError> {
    let trimmed = body.name.trim();
    if trimmed.is_empty() || trimmed.len() > 64 {
        return Err(ApiError::bad_request("guild name must be 1..64 chars"));
    }

    let guild = db::create_guild(&state.db, user.id, trimmed)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to create guild: {e}")))?;
    Ok((StatusCode::CREATED, Json(guild)))
}

#[derive(Deserialize)]
struct CreateInviteBody {
    max_uses: Option<i32>,
    expires_in_hours: Option<i64>,
}

async fn create_invite(
    State(state): State<AppState>,
    user: AuthUser,
    Path(guild_id): Path<Uuid>,
    Json(body): Json<CreateInviteBody>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_guild_member(&state, guild_id, user.id).await?;

    if let Some(v) = body.max_uses
        && v <= 0
    {
        return Err(ApiError::bad_request("max_uses must be > 0"));
    }

    if let Some(v) = body.expires_in_hours
        && v <= 0
    {
        return Err(ApiError::bad_request("expires_in_hours must be > 0"));
    }

    let invite = db::create_invite(
        &state.db,
        guild_id,
        user.id,
        body.max_uses,
        body.expires_in_hours,
    )
    .await
    .map_err(|e| ApiError::internal(&format!("failed to create invite: {e}")))?;

    Ok((StatusCode::CREATED, Json(invite)))
}

async fn join_invite(
    State(state): State<AppState>,
    user: AuthUser,
    Path(code): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
    let guild = db::join_invite(&state.db, &code.to_uppercase(), user.id)
        .await
        .map_err(|e| ApiError::bad_request(&format!("failed to join invite: {e}")))?;

    Ok(Json(guild))
}

async fn list_channels(
    State(state): State<AppState>,
    user: AuthUser,
    Path(guild_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_guild_member(&state, guild_id, user.id).await?;

    let channels = db::list_channels_for_guild(&state.db, guild_id)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to list channels: {e}")))?;
    Ok(Json(channels))
}

#[derive(Serialize)]
struct GuildVoicePresenceItem {
    channel_id: Uuid,
    participants: Vec<voice::VoiceParticipant>,
}

#[derive(Serialize)]
struct GuildVoicePresenceResponse {
    channels: Vec<GuildVoicePresenceItem>,
}

async fn guild_voice_presence(
    State(state): State<AppState>,
    user: AuthUser,
    Path(guild_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_guild_member(&state, guild_id, user.id).await?;

    let voice_channels = db::list_voice_channels_for_guild(&state.db, guild_id)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to list voice channels: {e}")))?;

    let mut channels = Vec::with_capacity(voice_channels.len());
    for ch in voice_channels {
        channels.push(GuildVoicePresenceItem {
            channel_id: ch.id,
            participants: state.voice.participants(ch.id).await,
        });
    }

    Ok(Json(GuildVoicePresenceResponse { channels }))
}

#[derive(Deserialize)]
struct CreateChannelBody {
    guild_id: Uuid,
    name: String,
    kind: Option<String>,
}

async fn create_channel(
    State(state): State<AppState>,
    user: AuthUser,
    Json(body): Json<CreateChannelBody>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_guild_member(&state, body.guild_id, user.id).await?;

    let trimmed = body.name.trim();
    if trimmed.is_empty() || trimmed.len() > 64 {
        return Err(ApiError::bad_request("channel name must be 1..64 chars"));
    }

    let kind = body.kind.unwrap_or_else(|| db::CHANNEL_KIND_TEXT.to_string());
    if kind != db::CHANNEL_KIND_TEXT && kind != db::CHANNEL_KIND_VOICE {
        return Err(ApiError::bad_request("channel kind must be 'text' or 'voice'"));
    }

    let channel = db::create_channel(&state.db, body.guild_id, trimmed, &kind)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to create channel: {e}")))?;
    Ok((StatusCode::CREATED, Json(channel)))
}

#[derive(Deserialize)]
struct ListMessagesQuery {
    limit: Option<i64>,
}

async fn list_messages(
    State(state): State<AppState>,
    user: AuthUser,
    Path(channel_id): Path<Uuid>,
    Query(query): Query<ListMessagesQuery>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_channel_member(&state, channel_id, user.id).await?;

    let limit = query.limit.unwrap_or(50).clamp(1, 200);

    let messages = db::list_messages(&state.db, channel_id, limit)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to list messages: {e}")))?;

    Ok(Json(messages))
}

#[derive(Deserialize)]
struct SendMessageBody {
    body: String,
}

#[derive(Serialize)]
struct SendMessageResponse {
    ok: bool,
}

async fn send_message(
    State(state): State<AppState>,
    user: AuthUser,
    Path(channel_id): Path<Uuid>,
    Json(body): Json<SendMessageBody>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_channel_member(&state, channel_id, user.id).await?;

    let content = body.body.trim();
    if content.is_empty() || content.len() > 4000 {
        return Err(ApiError::bad_request("message body must be 1..4000 chars"));
    }

    db::create_message(&state.db, channel_id, user.id, content)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to create message: {e}")))?;

    Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true })))
}

async fn list_dm_messages(
    State(state): State<AppState>,
    user: AuthUser,
    Path(other_user_id): Path<Uuid>,
    Query(query): Query<ListMessagesQuery>,
) -> Result<impl IntoResponse, ApiError> {
    if other_user_id == user.id {
        return Err(ApiError::bad_request("cannot open dm with yourself"));
    }

    let other_user_exists = db::user_exists(&state.db, other_user_id)
        .await
        .map_err(|e| ApiError::internal(&format!("user check failed: {e}")))?;
    if !other_user_exists {
        return Err(ApiError {
            status: StatusCode::NOT_FOUND,
            message: "user not found".to_string(),
        });
    }

    let limit = query.limit.unwrap_or(50).clamp(1, 200);
    let messages = db::list_direct_messages(&state.db, user.id, other_user_id, limit)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to list dm messages: {e}")))?;

    Ok(Json(messages))
}

async fn send_dm_message(
    State(state): State<AppState>,
    user: AuthUser,
    Path(other_user_id): Path<Uuid>,
    Json(body): Json<SendMessageBody>,
) -> Result<impl IntoResponse, ApiError> {
    if other_user_id == user.id {
        return Err(ApiError::bad_request("cannot send dm to yourself"));
    }

    let other_user_exists = db::user_exists(&state.db, other_user_id)
        .await
        .map_err(|e| ApiError::internal(&format!("user check failed: {e}")))?;
    if !other_user_exists {
        return Err(ApiError {
            status: StatusCode::NOT_FOUND,
            message: "user not found".to_string(),
        });
    }

    let content = body.body.trim();
    if content.is_empty() || content.len() > 4000 {
        return Err(ApiError::bad_request("message body must be 1..4000 chars"));
    }

    db::create_direct_message(&state.db, user.id, other_user_id, content)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to create dm message: {e}")))?;

    Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true })))
}

async fn voice_ws(
    ws: WebSocketUpgrade,
    State(state): State<AppState>,
    user: AuthUser,
    Path(channel_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiError> {
    ensure_channel_member(&state, channel_id, user.id).await?;

    let channel = db::get_channel_by_id(&state.db, channel_id)
        .await
        .map_err(|e| ApiError::internal(&format!("failed to load channel: {e}")))?
        .ok_or_else(|| ApiError {
            status: StatusCode::NOT_FOUND,
            message: "channel not found".to_string(),
        })?;

    if channel.kind != db::CHANNEL_KIND_VOICE {
        return Err(ApiError::bad_request("voice websocket requires a voice channel"));
    }

    Ok(ws.on_upgrade(move |socket| {
        voice::handle_socket(state, socket, channel_id, user.id)
    }))
}

async fn ensure_guild_member(state: &AppState, guild_id: Uuid, user_id: Uuid) -> Result<(), ApiError> {
    let is_member = db::is_member_of_guild(&state.db, guild_id, user_id)
        .await
        .map_err(|e| ApiError::internal(&format!("membership check failed: {e}")))?;

    if !is_member {
        return Err(ApiError {
            status: StatusCode::FORBIDDEN,
            message: "not a member of this guild".to_string(),
        });
    }

    Ok(())
}

async fn ensure_channel_member(state: &AppState, channel_id: Uuid, user_id: Uuid) -> Result<(), ApiError> {
    let guild_id = db::guild_id_for_channel(&state.db, channel_id)
        .await
        .map_err(|e| ApiError::internal(&format!("channel lookup failed: {e}")))?
        .ok_or_else(|| ApiError {
            status: StatusCode::NOT_FOUND,
            message: "channel not found".to_string(),
        })?;

    ensure_guild_member(state, guild_id, user_id).await
}
