use crate::error::Result;
use chrono::{DateTime, Utc};
use redis::{AsyncCommands, RedisResult};
use serde::{Deserialize, Serialize};
use siren_core::{data, utils::csprng};
use std::{env, sync::OnceLock};
use uuid::Uuid;

static SESSION_TTL: OnceLock<i64> = OnceLock::new();

pub fn get_session_ttl() -> i64 {
  *SESSION_TTL.get_or_init(|| {
    env::var("API_SESSION_TTL")
      .ok()
      .and_then(|val| val.parse::<i64>().ok())
      .unwrap_or(86400) // 24 hours
  })
}

/// A server-side session stored in Redis.
///
/// Contains the user's identity and a `fingerprint` (SHA-256 of
/// `{client_ip}:{user_agent}`) so that stolen cookies can be detected.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Session {
  pub session_id: String,
  pub user_id: Uuid,
  pub user_name: String,
  /// SHA-256 hex of `{client_ip}:{user_agent}` captured at login time.
  pub fingerprint: String,
  pub expires_at: DateTime<Utc>,
}

impl Session {
  pub fn new(user_id: Uuid, user_name: String, fingerprint: String) -> Session {
    let now = Utc::now();
    let session_ttl = get_session_ttl();
    Session {
      session_id: csprng(32),
      user_id,
      user_name,
      fingerprint,
      expires_at: now + chrono::Duration::seconds(session_ttl),
    }
  }

  pub async fn insert(&self) -> Result<()> {
    let mut redis = data::redis_async_connection().await?;
    let session_id = self.session_id.clone();
    let session_ttl = get_session_ttl();
    redis
      .set_ex::<_, _, ()>(session_id, serde_json::to_string(self)?, session_ttl as u64)
      .await?;
    Ok(())
  }

  pub async fn find(session_id: &str) -> Result<Option<Session>> {
    let mut redis = data::redis_async_connection().await?;
    let result: RedisResult<Option<String>> = redis.get(session_id).await;
    match result {
      Ok(Some(value)) => Ok(Some(serde_json::from_str(&value)?)),
      Ok(None) => Ok(None),
      Err(err) => Err(err.into()),
    }
  }

  pub async fn delete(session_id: &str) -> Result<()> {
    let mut redis = data::redis_async_connection().await?;
    let result: RedisResult<()> = redis.del(session_id).await;
    match result {
      Ok(_) => Ok(()),
      Err(err) => Err(err.into()),
    }
  }
}
