script 0.5.0

barebones http scripting
pub mod parse;

use anyhow::{anyhow, Error};
use chrono::{DateTime, Duration, Utc};
use dashmap::DashMap;
use global_placeholders::global;
use macros_rs::{fmt::string, fs::file_exists, obj::lazy_lock};
use md5::{Digest, Md5};
use serde::{Deserialize, Serialize};
use smartstring::{LazyCompact, SmartString};
use tokio::sync::Mutex;
use walkdir::WalkDir;

use tokio::fs::{read, write};

use std::{
    collections::{HashMap, HashSet},
    fs::{create_dir_all, read_dir, remove_dir, remove_file},
    mem::take,
    path::{Path, PathBuf},
    sync::Arc,
};

pub type RtTime = DateTime<Utc>;
pub type RtIndex = (String, Route);
pub type RtData = SmartString<LazyCompact>;
pub type RtArgs = Option<Vec<RtData>>;
pub type RtConfig = Option<HashMap<String, String>>;
pub type RtGlobalIndex = Arc<Mutex<DashMap<String, RouteContainer>>>;

pub enum RtKind {
    Normal,
    Wildcard,
    NotFound,
}

pub struct RouteContainer {
    pub inner: Route,
    present_in_current_update: bool,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct Route {
    pub cfg: RtConfig,
    pub args: RtArgs,
    pub hash: String,
    pub expires: RtTime,
    pub created: RtTime,
    pub cache: PathBuf,
    pub route: RtData,
    pub fn_name: RtData,
    pub fn_body: RtData,
    pub start_pos: usize,
    pub end_pos: usize,
}

lazy_lock! {
    pub static ROUTES_INDEX: RtGlobalIndex = Arc::new(Mutex::new(DashMap::new()));
}

async fn get_fallback_route() -> Option<(Route, Vec<String>)> {
    let fallback_routes = [("not_found", "__handler_not_found"), ("wildcard", "__handler_wildcard")];

    let page_exists = |key| match key {
        "not_found" => file_exists!(&global!("dirs.handler", "/not_found")),
        "wildcard" => file_exists!(&global!("dirs.handler", "/wildcard")),
        _ => false,
    };

    for (page, handler) in fallback_routes {
        if page_exists(page) {
            if let Ok(route) = Route::get(handler).await {
                return Some((route, vec![]));
            }
        }
    }

    None
}

async fn match_route(route_template: &str, placeholders: &Vec<RtData>, url: &str) -> Option<Vec<String>> {
    let mut matched_placeholders = Vec::new();

    let route_segments: Vec<&str> = route_template.split('/').collect();
    let url_segments: Vec<&str> = url.split('/').collect();

    if route_segments.len() != url_segments.len() {
        return None;
    }

    for (route_segment, url_segment) in route_segments.iter().zip(url_segments.iter()) {
        if let Some(placeholder_value) = match_segment(route_segment, url_segment, placeholders).await {
            if !placeholder_value.is_empty() {
                matched_placeholders.push(placeholder_value);
            }
        } else {
            return None;
        }
    }

    Some(matched_placeholders)
}

async fn match_segment<'a>(route_segment: &'a str, url_segment: &'a str, placeholders: &Vec<RtData>) -> Option<String> {
    if route_segment.starts_with('{') && route_segment.ends_with('}') {
        let placeholder = route_segment[1..route_segment.len() - 1].into();
        if placeholders.contains(&placeholder) {
            Some(url_segment.to_string())
        } else {
            None
        }
    } else if route_segment == url_segment {
        Some("".to_string())
    } else {
        let route_parts: Vec<&str> = route_segment.split('.').collect();
        let url_parts: Vec<&str> = url_segment.split('.').collect();
        if route_parts.len() == url_parts.len() && route_parts.last() == url_parts.last() {
            Box::pin(match_segment(route_parts[0], url_parts[0], placeholders)).await
        } else {
            None
        }
    }
}

impl Route {
    pub fn default() -> Self { Default::default() }

    pub async fn search_for(url: &str) -> Option<(Route, Vec<String>)> {
        let index = ROUTES_INDEX.lock().await;

        for entry in index.iter() {
            let (_, route_container) = entry.pair();
            let route_template = &route_container.inner.route;

            let placeholders = match &route_container.inner.args {
                Some(args) => args,
                None => &vec![],
            };

            if let Some(matched_values) = match_route(route_template, &placeholders, url).await {
                println!("found: {url}");
                return Some((route_container.inner.clone(), matched_values));
            }
        }

        get_fallback_route().await
    }

    pub async fn cleanup() -> std::io::Result<()> {
        let cache_dir = PathBuf::from(global!("base.cache"));
        let routes = ROUTES_INDEX.lock().await;

        tracing::trace!("Cache directory: {:?}", cache_dir);

        let valid_cache_files: HashSet<PathBuf> = routes
            .iter()
            .map(|item| {
                let path = item.value().inner.cache.clone();
                tracing::trace!("Valid cache file: {:?}", path);
                path
            })
            .collect();

        for entry in WalkDir::new(&cache_dir).into_iter().filter_map(|e| e.ok()) {
            let path = entry.path().to_path_buf();
            if path.is_file() {
                tracing::trace!("Checking file: {:?}", path);

                let should_keep = valid_cache_files.iter().any(|valid_path| {
                    let paths_match = path == *valid_path;
                    tracing::trace!("Comparing {:?} with {:?}: {}", path, valid_path, paths_match);
                    paths_match
                });

                if !should_keep {
                    tracing::debug!("Deleting file: {:?}", path);
                    remove_file(&path)?;
                } else {
                    tracing::debug!("Keeping file: {:?}", path);
                }
            } else if entry.file_type().is_dir() {
                if read_dir(path.to_owned())?.next().is_none() {
                    tracing::debug!("Removing empty directory: {:?}", path);
                    remove_dir(&path)?;
                }
            }
        }

        Ok(())
    }

    pub async fn update_index(new_routes: Vec<RtIndex>) {
        let routes = ROUTES_INDEX.lock().await;

        for mut entry in routes.iter_mut() {
            entry.present_in_current_update = false;
        }

        for (key, value) in new_routes {
            routes
                .entry(key)
                .and_modify(|e| {
                    e.inner = value.to_owned();
                    e.present_in_current_update = true;
                })
                .or_insert(RouteContainer {
                    inner: value,
                    present_in_current_update: true,
                });
        }

        routes.retain(|_, v| v.present_in_current_update);
    }

    pub fn cache(&mut self, kind: RtKind) -> &Self {
        let now = Utc::now();
        let mut md5 = Md5::new();

        let route_name = match kind {
            RtKind::Wildcard => "/wildcard",
            RtKind::NotFound => "/not_found",
            RtKind::Normal => self.route.as_str(),
        };

        let cache_key = match kind {
            RtKind::Wildcard => global!("dirs.handler", route_name),
            RtKind::NotFound => global!("dirs.handler", route_name),
            RtKind::Normal => global!("dirs.cache", route_name),
        };

        let fn_name = match kind {
            RtKind::Wildcard => "wildcard",
            RtKind::NotFound => "not_found",
            RtKind::Normal => self.fn_name.as_str(),
        };

        self.route = route_name.into();
        self.fn_name = fn_name.replace("/", "_").replace(".", "_d").into();

        md5.update(&self.route);
        md5.update(&self.fn_name);
        md5.update(&self.fn_body);

        self.created = now;
        self.expires = now + Duration::hours(3);
        self.cache = Path::new(&cache_key).to_owned();
        self.hash = const_hex::encode(md5.finalize());

        return self;
    }

    // save functions that expired or dont exist
    pub async fn save(&mut self, kind: RtKind) -> RtIndex {
        self.cache(kind);

        if let Some(parent) = self.cache.parent() {
            if !parent.exists() {
                // add error handling
                create_dir_all(parent).unwrap();
            }
        }

        let encoded = match ron::ser::to_string(&self) {
            Ok(contents) => contents,
            Err(err) => {
                tracing::error!(err = string!(err), "Cannot encode route");
                std::process::exit(1);
            }
        };

        if let Err(err) = write(self.cache.to_owned(), encoded).await {
            tracing::error!(err = string!(err), "Error writing route");
            std::process::exit(1);
        }

        ROUTES_INDEX.lock().await.insert(
            self.hash.to_owned(),
            RouteContainer {
                inner: self.clone(),
                present_in_current_update: true,
            },
        );

        return (self.hash.to_owned(), take(self));
    }

    pub async fn get(key: &str) -> Result<Route, Error> {
        let key = match key {
            "/" => global!("dirs.cache", "/index"),
            "__handler_not_found" => global!("dirs.handler", "/not_found"),
            "__handler_wildcard" => global!("dirs.handler", "/wildcard"),
            _ => global!("dirs.cache", key),
        };

        let bytes = match read(&key).await {
            Ok(contents) => contents,
            Err(err) => return Err(anyhow!(err)),
        };

        Ok(ron::de::from_bytes(&bytes)?)
    }

    pub fn construct_fn(&self) -> String {
        let args = match self.args.to_owned() {
            Some(args) => match args.len() {
                0 => String::new(),
                1 => args[0].to_string(),
                _ => args.join(", "),
            },
            None => "".into(),
        };

        format!("fn {}({args}){{{}}}", self.fn_name, self.fn_body)
    }
}

pub mod prelude {
    pub use super::Route;
}