//! gRPC AuthService implementation — Login and RefreshToken RPCs.
//!
//! This service is exposed WITHOUT the auth interceptor since it handles
//! pre-authentication token issuance.

use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use tonic::{Request, Response, Status};
use tracing::{error, info};
use uuid::Uuid;

use crate::auth::jwt::service::{JwtClaims, JwtConfig};
use crate::auth_proto::auth_service_server::AuthService;
use crate::auth_proto::{
    LoginRequest, LoginResponse, RefreshTokenRequest, RefreshTokenResponse,
};

/// Access token lifetime: 1 hour (seconds).
const ACCESS_TOKEN_EXPIRY_SECS: u64 = 3600;

/// Refresh token lifetime: 24 hours (seconds).
const REFRESH_TOKEN_EXPIRY_SECS: u64 = 86400;

/// gRPC implementation of `auth.AuthService`.
pub struct AuthGrpcService {
    jwt_config: Arc<JwtConfig>,
}

impl AuthGrpcService {
    /// Create a new `AuthGrpcService` backed by the given JWT configuration.
    pub fn new(jwt_config: Arc<JwtConfig>) -> Self {
        Self { jwt_config }
    }

    /// Validate user credentials.
    ///
    /// Checks the `USER_CREDENTIALS` environment variable (format: `user:pass`)
    /// first, then falls back to a hard-coded development user (`admin` / `admin`).
    fn validate_credentials(username: &str, password: &str) -> Result<(), Status> {
        // Check USER_CREDENTIALS env var (format: "user:pass")
        if let Ok(creds) = std::env::var("USER_CREDENTIALS") {
            if let Some((env_user, env_pass)) = creds.split_once(':') {
                if username == env_user && password == env_pass {
                    return Ok(());
                }
            }
            // Env var set but credentials don't match
            return Err(Status::unauthenticated("invalid credentials"));
        }

        // Development fallback: admin/admin
        if username == "admin" && password == "admin" {
            error!("Login accepted using hard-coded dev credentials (set USER_CREDENTIALS for production)");
            return Ok(());
        }

        Err(Status::unauthenticated("invalid credentials"))
    }

    /// Build and sign a JWT token (access or refresh).
    fn issue_token(
        &self,
        subject: &str,
        token_type: &str,
        session_id: &str,
        expiry_secs: u64,
    ) -> Result<(String, u64), Status> {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|e| {
                error!("System time error: {}", e);
                Status::internal("system time error")
            })?
            .as_secs();

        let exp = now.checked_add(expiry_secs).ok_or_else(|| {
            error!("Overflow computing token expiry");
            Status::internal("token expiry overflow")
        })?;

        let claims = JwtClaims {
            jti: Uuid::new_v4().to_string(),
            sub: subject.to_string(),
            iat: now,
            exp,
            iss: self.jwt_config.jwt_issuer.clone(),
            aud: self.jwt_config.jwt_audience.clone(),
            roles: vec!["user".to_string()],
            permissions: vec!["api.access".to_string()],
            token_type: token_type.to_string(),
            session_id: Some(session_id.to_string()),
        };

        let header = Header::new(Algorithm::HS256);
        let key = EncodingKey::from_secret(self.jwt_config.jwt_secret.as_bytes());
        let token = encode(&header, &claims, &key).map_err(|e| {
            error!("Failed to encode JWT: {}", e);
            Status::internal("failed to issue token")
        })?;

        Ok((token, exp))
    }

    /// Decode and validate a refresh token, returning its claims.
    fn decode_refresh_token(&self, token: &str) -> Result<JwtClaims, Status> {
        if token.is_empty() {
            return Err(Status::invalid_argument("refresh token is empty"));
        }

        if token.len() > 8192 {
            return Err(Status::invalid_argument("refresh token too long"));
        }

        let key = DecodingKey::from_secret(self.jwt_config.jwt_secret.as_bytes());

        let mut validation = Validation::new(Algorithm::HS256);
        validation.set_issuer(&[&self.jwt_config.jwt_issuer]);
        validation.set_audience(&[&self.jwt_config.jwt_audience]);
        validation.validate_exp = true;
        validation.validate_nbf = false;
        validation.leeway = 10;
        validation.validate_aud = true;

        let token_data = decode::<JwtClaims>(token, &key, &validation).map_err(|e| {
            error!("Refresh token decode failed: {}", e);
            Status::unauthenticated("invalid refresh token")
        })?;

        if token_data.claims.token_type != "refresh" {
            return Err(Status::unauthenticated(
                "token is not a refresh token",
            ));
        }

        if token_data.claims.sub.is_empty() {
            return Err(Status::unauthenticated("refresh token has empty subject"));
        }

        Ok(token_data.claims)
    }
}

#[tonic::async_trait]
impl AuthService for AuthGrpcService {
    async fn login(
        &self,
        request: Request<LoginRequest>,
    ) -> Result<Response<LoginResponse>, Status> {
        let req = request.into_inner();

        if req.username.is_empty() || req.password.is_empty() {
            return Err(Status::invalid_argument(
                "username and password are required",
            ));
        }

        Self::validate_credentials(&req.username, &req.password)?;

        let session_id = Uuid::new_v4().to_string();

        let (access_token, expires_at) =
            self.issue_token(&req.username, "access", &session_id, ACCESS_TOKEN_EXPIRY_SECS)?;
        let (refresh_token, _) =
            self.issue_token(&req.username, "refresh", &session_id, REFRESH_TOKEN_EXPIRY_SECS)?;

        info!(user = %req.username, "Login successful, tokens issued");

        Ok(Response::new(LoginResponse {
            access_token,
            refresh_token,
            expires_at: i64::try_from(expires_at).unwrap_or(i64::MAX),
        }))
    }

    async fn refresh_token(
        &self,
        request: Request<RefreshTokenRequest>,
    ) -> Result<Response<RefreshTokenResponse>, Status> {
        let req = request.into_inner();
        let claims = self.decode_refresh_token(&req.refresh_token)?;

        let session_id = claims
            .session_id
            .unwrap_or_else(|| Uuid::new_v4().to_string());

        let (access_token, expires_at) =
            self.issue_token(&claims.sub, "access", &session_id, ACCESS_TOKEN_EXPIRY_SECS)?;
        let (refresh_token, _) =
            self.issue_token(&claims.sub, "refresh", &session_id, REFRESH_TOKEN_EXPIRY_SECS)?;

        info!(user = %claims.sub, "Token refreshed successfully");

        Ok(Response::new(RefreshTokenResponse {
            access_token,
            refresh_token,
            expires_at: i64::try_from(expires_at).unwrap_or(i64::MAX),
        }))
    }
}
