use crate::api::error::ApiError;
use crate::api::state::AppState;
use async_trait::async_trait;
use axum::extract::{FromRequest, RequestParts};
use axum::http::header;
use std::sync::Arc;

/// Cookie name used by the admin dashboard. Browser-set, HttpOnly, SameSite=Strict.
pub const SESSION_COOKIE: &str = "rd_admin_session";

pub struct AuthedUser {
    pub user_id: i64,
    pub name: String,
    pub is_admin: bool,
}

pub fn sha256_token(token: &str) -> Vec<u8> {
    sodiumoxide::crypto::hash::sha256::hash(token.as_bytes())
        .as_ref()
        .to_vec()
}

#[async_trait]
impl<B: Send> FromRequest<B> for AuthedUser {
    type Rejection = ApiError;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        let state: axum::extract::Extension<Arc<AppState>> =
            axum::extract::Extension::from_request(req)
                .await
                .map_err(|_| ApiError::Internal("missing state".into()))?;
        let token = extract_token(req).ok_or(ApiError::Unauthorized)?;
        let sha = sha256_token(&token);

        let (user_id, _exp) = state
            .db
            .token_lookup(&sha)
            .await
            .map_err(|e| ApiError::Internal(e.to_string()))?
            .ok_or(ApiError::Unauthorized)?;

        // Slide the expiry forward on every authenticated request.
        if let Err(e) = state.db.token_touch(&sha, state.cfg.session_ttl_secs).await {
            hbb_common::log::warn!("token_touch failed: {}", e);
        }

        let user = state
            .db
            .user_find_by_id(user_id)
            .await
            .map_err(|e| ApiError::Internal(e.to_string()))?
            .ok_or(ApiError::Unauthorized)?;

        Ok(Self {
            user_id: user.id,
            name: user.username,
            is_admin: user.is_admin,
        })
    }
}

/// Extract a token from either the `Authorization: Bearer …` header (preferred,
/// for the desktop client and curl) or the `rd_admin_session` cookie (for the
/// browser-driven admin dashboard). Returns `None` if neither is present.
fn extract_token<B>(req: &RequestParts<B>) -> Option<String> {
    // Bearer header wins so a curl smoke test always behaves predictably,
    // even when run from the same browser session.
    if let Some(auth) = req.headers().get(header::AUTHORIZATION) {
        if let Ok(s) = auth.to_str() {
            if let Some(tok) = s.strip_prefix("Bearer ").map(str::trim) {
                if !tok.is_empty() {
                    return Some(tok.to_string());
                }
            }
        }
    }
    // Cookie header is a single line: `name=value; name2=value2; …`. Walk
    // the comma-or-semicolon-separated pairs without pulling in a cookie crate.
    if let Some(cookie_hdr) = req.headers().get(header::COOKIE) {
        if let Ok(s) = cookie_hdr.to_str() {
            for pair in s.split(';') {
                let pair = pair.trim();
                if let Some((name, value)) = pair.split_once('=') {
                    if name.trim() == SESSION_COOKIE {
                        let v = value.trim();
                        if !v.is_empty() {
                            return Some(v.to_string());
                        }
                    }
                }
            }
        }
    }
    None
}
