Skip to content
use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: String,
    pub email: String,
    pub password_hash: String,
    pub created_at: u64,
    pub updated_at: u64,
}

impl User {
    pub fn new(email: &str, password: &str) -> Result<Self, String> {
        let salt = SaltString::generate(&mut OsRng);
        let argon2 = Argon2::default();
        let hash = argon2
            .hash_password(password.as_bytes(), &salt)
            .map_err(|e| e.to_string())?
            .to_string();

        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        Ok(Self {
            id: Uuid::new_v4().to_string(),
            email: email.to_string(),
            password_hash: hash,
            created_at: now,
            updated_at: now,
        })
    }

    pub fn verify_password(&self, password: &str) -> bool {
        let parsed = match PasswordHash::new(&self.password_hash) {
            Ok(h) => h,
            Err(_) => return false,
        };
        Argon2::default()
            .verify_password(password.as_bytes(), &parsed)
            .is_ok()
    }

    pub fn change_password(&mut self, new_password: &str) -> Result<(), String> {
        let salt = SaltString::generate(&mut OsRng);
        let argon2 = Argon2::default();
        self.password_hash = argon2
            .hash_password(new_password.as_bytes(), &salt)
            .map_err(|e| e.to_string())?
            .to_string();
        self.updated_at = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        Ok(())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ticket {
    pub id: String,
    pub code: String,
    pub created_by: String,
    pub used_by: Option<String>,
    pub used_at: Option<u64>,
    pub created_at: u64,
}

impl Ticket {
    pub fn new(created_by: &str) -> Self {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        Self {
            id: Uuid::new_v4().to_string(),
            code: Uuid::new_v4()
                .to_string()
                .replace("-", "")
                .to_uppercase(),
            created_by: created_by.to_string(),
            used_by: None,
            used_at: None,
            created_at: now,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
    pub token: String,
    pub user_id: String,
    pub created_at: u64,
    pub expires_at: u64,
}

impl Session {
    pub fn new(user_id: &str) -> Self {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        Self {
            token: Uuid::new_v4().to_string(),
            user_id: user_id.to_string(),
            created_at: now,
            expires_at: now + 7 * 24 * 60 * 60,
        }
    }

    pub fn is_expired(&self) -> bool {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();
        now > self.expires_at
    }

    pub fn cookie_max_age_secs(&self) -> u64 {
        if self.expires_at > self.created_at {
            self.expires_at - self.created_at
        } else {
            0
        }
    }
}

pub const SESSION_COOKIE: &str = "corium_session";
pub const ADMIN_API_KEY_COOKIE: &str = "corium_admin_key";

#[derive(Debug, Clone)]
pub struct HurlTest {
    pub method: String,
    pub path: String,
    pub params: std::collections::HashMap<String, String>,
    pub body: Option<String>,
}

impl HurlTest {
    pub fn parse(input: &str) -> Result<Self, String> {
        let lines: Vec<&str> = input.lines().collect();
        if lines.is_empty() {
            return Err("Empty test input".to_string());
        }

        let first_line = lines[0].trim();
        if first_line.is_empty() {
            return Err("First line is empty".to_string());
        }

        let parts: Vec<&str> = first_line.splitn(2, ' ').collect();
        if parts.is_empty() {
            return Err("Invalid format. Expected: METHOD /path".to_string());
        }

        let method = if parts.len() == 2 {
            parts[0].to_uppercase()
        } else {
            "GET".to_string()
        };

        let url = if parts.len() >= 2 {
            parts[1].to_string()
        } else {
            parts[0].to_string()
        };

        let mut params = std::collections::HashMap::new();
        let mut body = None;

        let path = if let Some(query_start) = url.find('?') {
            let path = url[..query_start].to_string();
            let query = &url[query_start + 1..];
            for pair in query.split('&') {
                if let Some(eq_pos) = pair.find('=') {
                    let key = pair[..eq_pos].to_string();
                    let value = pair[eq_pos + 1..].to_string();
                    params.insert(key, value);
                } else if !pair.is_empty() {
                    params.insert(pair.to_string(), String::new());
                }
            }
            path
        } else {
            url
        };

        let mut in_body = false;
        let mut body_lines = Vec::new();
        for line in lines.iter().skip(1) {
            let line = line.trim();
            if line.is_empty() {
                if !body_lines.is_empty() || in_body {
                    in_body = true;
                }
                continue;
            }
            if in_body {
                body_lines.push(line.to_string());
            } else if line.contains('=') {
                if let Some(eq_pos) = line.find('=') {
                    let key = line[..eq_pos].to_string();
                    let value = line[eq_pos + 1..].to_string();
                    params.insert(key, value);
                }
            }
        }

        if !body_lines.is_empty() {
            body = Some(body_lines.join("\n"));
        }

        Ok(Self {
            method,
            path,
            params,
            body,
        })
    }
}

pub fn parse_simple_params(input: &str) -> std::collections::HashMap<String, String> {
    let mut params = std::collections::HashMap::new();
    for line in input.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        if let Some(eq_pos) = line.find('=') {
            let key = line[..eq_pos].to_string();
            let value = line[eq_pos + 1..].to_string();
            params.insert(key, value);
        }
    }
    params
}