use crate::{
  AppState,
  app_state::DiscordOAuthState,
  auth::{
    SessionAuthorization,
    local::{build_session_cookie, issue_jwt},
    middleware::{compute_fingerprint, extract_ip},
    session::Session,
  },
  error::{Error, Result},
};
use axum::{
  Router,
  extract::{Query, State},
  http::{HeaderMap, StatusCode},
  response::{IntoResponse, Redirect},
  routing::get,
};
use axum_extra::extract::CookieJar;
use serde::{Deserialize, Serialize};
use siren_core::utils::csprng;
use std::sync::Arc;
use uuid::Uuid;

const DISCORD_REDIRECT_PATH: &str = "/api/auth/discord/callback";

pub fn get_routes() -> Router<Arc<AppState>> {
  Router::new()
    .route("/authorize", get(discord_authorize))
    .route("/callback", get(discord_callback))
    .route("/connect", get(discord_connect))
}

#[derive(Deserialize, Debug)]
struct AuthorizeQuery {
  redirect_uri: String,
}

#[derive(Deserialize, Debug)]
struct CallbackQuery {
  code: String,
  state: Option<String>,
}

#[derive(Serialize, Deserialize)]
struct DiscordTokenResponse {
  access_token: String,
  token_type: String,
  expires_in: u64,
  refresh_token: String,
  scope: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct DiscordUser {
  id: String,
  username: String,
  discriminator: String,
  avatar: Option<String>,
}

/// Begin a Discord OAuth login flow (anonymous users)
///
/// Stores the caller's desired `redirect_uri` in the state cache so the
/// callback can redirect to the right place after login
async fn discord_authorize(
  State(state): State<Arc<AppState>>,
  Query(query): Query<AuthorizeQuery>,
) -> impl IntoResponse {
  let oauth_state = csprng(16);
  log::trace!("Discord authorize: {:?}, state={}", query, oauth_state);

  state.discord_authorize_cache.lock().await.insert(
    oauth_state.clone(),
    DiscordOAuthState {
      redirect_uri: query.redirect_uri,
      connecting_user_id: None,
    },
  );

  build_discord_oauth_url(&state, &oauth_state)
}

/// Begin a Discord OAuth connect flow (already-authenticated users).
///
/// The caller must have a valid session cookie. Their user ID is stored
/// in the state cache so the callback can link the Discord account to the
/// existing local account.
async fn discord_connect(
  State(state): State<Arc<AppState>>,
  Query(query): Query<AuthorizeQuery>,
  SessionAuthorization(session): SessionAuthorization,
) -> Result<impl IntoResponse> {
  let session = session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;
  let oauth_state = csprng(16);
  log::trace!(
    "Discord connect: {:?}, state={} (user_id={})",
    query,
    oauth_state,
    session.user_id
  );

  state.discord_authorize_cache.lock().await.insert(
    oauth_state.clone(),
    DiscordOAuthState {
      redirect_uri: query.redirect_uri,
      connecting_user_id: Some(session.user_id),
    },
  );

  Ok(build_discord_oauth_url(&state, &oauth_state))
}

fn build_discord_oauth_url(
  state: &AppState,
  oauth_state: &str,
) -> std::result::Result<String, StatusCode> {
  let discord_callback_url = format!("{}{}", state.base_url, DISCORD_REDIRECT_PATH);
  let encoded_callback = urlencoding_encode(&discord_callback_url);

  let url = format!(
    "https://discord.com/api/oauth2/authorize\
     ?client_id={}\
     &redirect_uri={}\
     &response_type=code\
     &scope=identify\
     &state={}",
    state.client_id, encoded_callback, oauth_state,
  );

  match serde_json::to_string(&url) {
    Ok(json) => {
      log::trace!("Discord OAuth URL: {}", json);
      Ok(json)
    }
    Err(e) => {
      log::error!("Failed to serialize Discord OAuth URL: {e}");
      Err(StatusCode::INTERNAL_SERVER_ERROR)
    }
  }
}

/// Very small percent-encoder for the callback URL (replaces `:` and `/`).
fn urlencoding_encode(s: &str) -> String {
  s.replace(':', "%3A").replace('/', "%2F")
}

/// Handle the Discord OAuth callback.
///
/// Two modes depending on what was stored in the state cache:
/// - **Login** (`connecting_user_id = None`): look up (or create) the local
///   user for this Discord account, then issue a session cookie and redirect.
/// - **Connect** (`connecting_user_id = Some(id)`): link the Discord account
///   to the existing local user, then redirect (no new session needed).
async fn discord_callback(
  State(state): State<Arc<AppState>>,
  Query(query): Query<CallbackQuery>,
  headers: HeaderMap,
  jar: CookieJar,
) -> impl IntoResponse {
  match do_oauth_callback(state, query, headers, jar).await {
    Ok(response) => response,
    Err((e, ui_redirect_uri)) => {
      log::error!("OAuth callback error: {:?}", e);
      let fallback = ui_redirect_uri.unwrap_or_else(|| "/".to_string());
      Redirect::temporary(&format!("{}?error=auth_failed", fallback)).into_response()
    }
  }
}

type CallbackErr = (Error, Option<String>);

async fn do_oauth_callback(
  state: Arc<AppState>,
  query: CallbackQuery,
  headers: HeaderMap,
  jar: CookieJar,
) -> std::result::Result<axum::response::Response, CallbackErr> {
  // Validate state & retrieve stored data
  let stored = {
    let mut cache = state.discord_authorize_cache.lock().await;
    match query.state {
      Some(ref s) => match cache.remove(s) {
        Some(v) => v,
        None => return Err((StatusCode::UNAUTHORIZED.into(), None)),
      },
      None => return Err((StatusCode::UNAUTHORIZED.into(), None)),
    }
  };
  log::trace!("Discord callback: query={:?} state={:?}", query, stored);

  let ui_redirect_uri = stored.redirect_uri.clone();
  let err_redirect = |s: StatusCode| -> std::result::Result<_, CallbackErr> {
    Err((s.into(), Some(ui_redirect_uri.clone())))
  };

  // The redirect_uri sent to Discord must exactly match /authorize
  let discord_callback_url = format!("{}{}", state.base_url, DISCORD_REDIRECT_PATH);

  // Exchange code for Discord access token
  let token_resp = state
    .client
    .post("https://discord.com/api/oauth2/token")
    .form(&[
      ("client_id", state.client_id.as_str()),
      ("client_secret", state.client_secret.as_str()),
      ("grant_type", "authorization_code"),
      ("code", query.code.as_str()),
      ("redirect_uri", discord_callback_url.as_str()),
    ])
    .send()
    .await
    .map_err(|_| err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err())?;

  if !token_resp.status().is_success() {
    log::error!("Token exchange failed: {:?}", token_resp.text().await);
    return err_redirect(StatusCode::INTERNAL_SERVER_ERROR);
  }

  let token_data: DiscordTokenResponse = token_resp
    .json()
    .await
    .map_err(|_| err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err())?;

  // Fetch Discord user info
  let user_resp = state
    .client
    .get("https://discord.com/api/users/@me")
    .bearer_auth(token_data.access_token)
    .send()
    .await
    .map_err(|_| err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err())?;

  if !user_resp.status().is_success() {
    log::error!("Discord user fetch failed: {:?}", user_resp.text().await);
    return err_redirect(StatusCode::INTERNAL_SERVER_ERROR);
  }

  let discord_user: DiscordUser = user_resp
    .json()
    .await
    .map_err(|_| err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err())?;

  log::debug!("Discord OAuth user: {:?}", discord_user);

  let pool = siren_core::data::pool();

  match stored.connecting_user_id {
    // Handle connecting an existing local user to a new Discord account
    Some(connecting_user_id) => {
      // Make sure this Discord account isn't already linked to a DIFFERENT user
      let existing_owner: Option<Uuid> = sqlx::query_scalar(
        "SELECT user_id FROM user_connections \
         WHERE provider = 'discord' AND provider_user_id = $1",
      )
      .bind(&discord_user.id)
      .fetch_optional(pool)
      .await
      .map_err(|e| {
        log::error!("DB error checking connection: {e}");
        err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
      })?;

      if let Some(owner_id) = existing_owner {
        if owner_id != connecting_user_id {
          return err_redirect(StatusCode::CONFLICT);
        }
      }

      // Upsert the connection
      sqlx::query(
        "INSERT INTO user_connections \
           (user_id, provider, provider_user_id, provider_username, provider_avatar) \
         VALUES ($1, 'discord', $2, $3, $4) \
         ON CONFLICT (user_id, provider) DO UPDATE \
           SET provider_user_id = EXCLUDED.provider_user_id, \
               provider_username = EXCLUDED.provider_username, \
               provider_avatar   = EXCLUDED.provider_avatar",
      )
      .bind(connecting_user_id)
      .bind(&discord_user.id)
      .bind(&discord_user.username)
      .bind(&discord_user.avatar)
      .execute(pool)
      .await
      .map_err(|e| {
        log::error!("DB error upserting connection: {e}");
        err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
      })?;

      // No new session — redirect back to account page with existing cookie
      Ok(Redirect::temporary(&ui_redirect_uri).into_response())
    }

    None => {
      // Find existing connection → local user_id
      let local_user_id: Option<(Uuid, String, String)> = sqlx::query_as(
        "SELECT u.id, u.username, u.status \
         FROM user_connections uc \
         JOIN users u ON u.id = uc.user_id \
         WHERE uc.provider = 'discord' AND uc.provider_user_id = $1",
      )
      .bind(&discord_user.id)
      .fetch_optional(pool)
      .await
      .map_err(|e| {
        log::error!("DB error looking up discord user: {e}");
        err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
      })?;

      let (user_id, username) = match local_user_id {
        // Already linked — use the existing local user
        Some(row) => {
          // Reject banned accounts
          if row.2 == "banned" {
            return err_redirect(StatusCode::FORBIDDEN);
          }
          // Keep provider fields up to date
          sqlx::query(
            "UPDATE user_connections \
             SET provider_username = $1, provider_avatar = $2 \
             WHERE user_id = $3 AND provider = 'discord'",
          )
          .bind(&discord_user.username)
          .bind(&discord_user.avatar)
          .bind(row.0)
          .execute(pool)
          .await
          .map_err(|e| {
            log::error!("DB error updating connection: {e}");
            err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
          })?;

          (row.0, row.1)
        }

        // First login — create a local user + connection
        None => {
          let base_username = &discord_user.username;
          let username = generate_unique_username(pool, base_username)
            .await
            .map_err(|e| {
              log::error!("DB error generating username: {e}");
              err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
            })?;

          // Create user (no password_hash — OAuth only)
          let new_id: Uuid =
            sqlx::query_scalar("INSERT INTO users (username) VALUES ($1) RETURNING id")
              .bind(&username)
              .fetch_one(pool)
              .await
              .map_err(|e| {
                log::error!("DB error creating user: {e}");
                err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
              })?;

          sqlx::query(
            "INSERT INTO user_connections \
               (user_id, provider, provider_user_id, provider_username, provider_avatar) \
             VALUES ($1, 'discord', $2, $3, $4)",
          )
          .bind(new_id)
          .bind(&discord_user.id)
          .bind(&discord_user.username)
          .bind(&discord_user.avatar)
          .execute(pool)
          .await
          .map_err(|e| {
            log::error!("DB error inserting connection: {e}");
            err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
          })?;

          (new_id, username)
        }
      };

      // Build fingerprint from the callback request's headers
      let ip = extract_ip(&headers);
      let user_agent = headers
        .get("user-agent")
        .and_then(|h| h.to_str().ok())
        .unwrap_or("unknown")
        .to_string();
      let fingerprint = compute_fingerprint(&ip, &user_agent);

      // Issue session
      let session = Session::new(user_id, username, fingerprint);
      session.insert().await.map_err(|e| {
        log::error!("Redis error inserting session: {e}");
        err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
      })?;

      let token = issue_jwt(&session).map_err(|e| {
        log::error!("JWT error: {e}");
        err_redirect(StatusCode::INTERNAL_SERVER_ERROR).unwrap_err()
      })?;

      let cookie = build_session_cookie(token);
      let new_jar = jar.add(cookie);

      Ok((new_jar, Redirect::temporary(&ui_redirect_uri)).into_response())
    }
  }
}

/// Return a username derived from `base` that does not yet exist in `users`.
async fn generate_unique_username(pool: &sqlx::PgPool, base: &str) -> crate::error::Result<String> {
  // Truncate to 28 chars to leave room for the `_XXXX` suffix
  let base = if base.len() > 28 { &base[..28] } else { base };

  let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE username = $1")
    .bind(base)
    .fetch_one(pool)
    .await?;

  if count == 0 {
    return Ok(base.to_string());
  }

  for _ in 0..20 {
    let candidate = format!("{}_{}", base, csprng(4));
    let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE username = $1")
      .bind(&candidate)
      .fetch_one(pool)
      .await?;
    if count == 0 {
      return Ok(candidate);
    }
  }

  Err(Error::internal_server_error(
    "Could not generate a unique username".into(),
  ))
}
