xrml 0.1.0

eXtensible Rust Markup Language — recursive acronym: HRML (HRML Markup Language) and TRML (TOML-like Markup Language)
Documentation
use crate::validation;
use std::fs;
use std::io;
use std::path::Path;
use xrml::{assets, config::Config, project::Project, router};

pub fn create_project(name: &str) -> io::Result<()> {
    let project_path = Path::new(name);
    if project_path.exists() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!("Directory '{}' already exists", name),
        ));
    }

    let config = Config {
        site_name: name.to_string(),
        ..Config::default()
    };

    fs::create_dir_all(project_path)?;
    setup_project_files(project_path, &config)
}

pub(crate) fn load_project_config(project_path: &Path) -> Result<Config, String> {
    let config_path = project_path.join("xrml.toml");
    if !config_path.exists() {
        return Err(format!("No xrml.toml found in {}", project_path.display()));
    }

    let content = fs::read_to_string(&config_path)
        .map_err(|e| format!("Failed to read {}: {}", config_path.display(), e))?;

    Config::from_toml(&content).map_err(|e| format!("Failed to load config: {}", e))
}

pub fn load_project(path: &Path) -> Result<Project, String> {
    let config = load_project_config(path)?;
    let templates_path = path.join(&config.templates_path);

    let mut project = Project::new(config).with_base_path(path);
    load_dir_into_project(&mut project, &templates_path, &templates_path)?;

    Ok(project)
}

fn load_dir_into_project(project: &mut Project, base: &Path, dir: &Path) -> Result<(), String> {
    let entries = fs::read_dir(dir).map_err(|e| format!("Failed to read dir: {}", e))?;
    for entry in entries {
        let entry = entry.map_err(|e| e.to_string())?;
        let path = entry.path();

        if path.is_dir() {
            load_dir_into_project(project, base, &path)?;
        } else if path
            .extension()
            .map(|e| e == "hrml" || e == "trml")
            .unwrap_or(false)
        {
            let rel_path = path
                .strip_prefix(base)
                .map_err(|e| e.to_string())?
                .to_string_lossy()
                .to_string();
            let content = fs::read_to_string(&path)
                .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
            project.add_file(rel_path, content);
        }
    }
    Ok(())
}

pub(crate) fn validate_project(path: &Path) -> Result<(), String> {
    let config_path = path.join("xrml.toml");
    if !config_path.exists() {
        return Err(format!("File not found: {}", config_path.display()));
    }

    let content = fs::read_to_string(&config_path)
        .map_err(|e| format!("Failed to read {}: {}", config_path.display(), e))?;
    let config =
        Config::from_toml(&content).map_err(|e| format!("Failed to load xrml.toml: {}", e))?;

    let templates_path = path.join(&config.templates_path);
    if !templates_path.exists() {
        return Err(format!(
            "Templates directory not found: {}\nCreate it or update xrml.toml",
            templates_path.display()
        ));
    }

    let base_layout = templates_path.join("layouts/base.hrml");
    if !base_layout.exists() {
        eprintln!(
            "[WARNING] Base layout not found at {}",
            base_layout.display()
        );
    }

    let index_page = templates_path.join("pages/index.hrml");
    if !index_page.exists() {
        eprintln!("[WARNING] Index page not found at {}", index_page.display());
    }

    let static_path = path.join(&config.static_path);
    if !static_path.exists() {
        eprintln!(
            "[WARNING] Static directory not found: {}",
            static_path.display()
        );
    }

    let mut project = Project::new(config.clone());
    load_dir_into_project(&mut project, &templates_path, &templates_path)?;

    match project.render("pages/index.hrml", &serde_json::json!({})) {
        Ok(_) => println!("[OK] Index template renders successfully"),
        Err(e) => eprintln!("[WARNING] Index template failed to render: {}", e),
    }

    println!("[OK] Project validation complete");
    Ok(())
}

pub fn check_project(path: &Path) -> Result<(), String> {
    let config = load_project_config(path)?;
    let templates_path = path.join(&config.templates_path);

    if !templates_path.exists() {
        return Err(format!(
            "Templates dir not found: {}",
            templates_path.display()
        ));
    }

    let pages_dir = templates_path.join("pages");
    let router = router::Router::from_pages_dir(&pages_dir);
    println!("Routes found: {}", router.routes.len());
    for route in &router.routes {
        println!("  {} -> {} ({:?})", route.path, route.template, route.kind);
    }

    let mut project = Project::new(config).with_base_path(path);
    load_dir_into_project(&mut project, &templates_path, &templates_path)?;
    project.parse_all().map_err(|e| e.to_string())?;

    let mut errors = 0;
    let mut ok = 0;

    for (page, id) in project.undefined_component_uses() {
        eprintln!("[ERROR] {page}: <?use id=\"{id}\"?> — no such component");
        errors += 1;
    }

    for route in router.static_routes() {
        let template_path = if route.template.starts_with("pages/") {
            route.template.clone()
        } else {
            format!("pages/{}", route.template)
        };

        match project.render(&template_path, &serde_json::json!({})) {
            Ok(html) => {
                if html.contains("<?") || html.contains("?>") {
                    eprintln!("[WARN] {} has unprocessed directives", route.path);
                }
                if html.contains("<!DOCTYPE") && html.matches("<html").count() != 1 {
                    eprintln!("[WARN] {} has malformed HTML", route.path);
                }
                ok += 1;
            }
            Err(e) => {
                eprintln!("[ERROR] {} failed: {}", route.path, e);
                errors += 1;
            }
        }
    }

    let cases_root = path.join("validation/cases");
    if cases_root.exists() {
        let records = validation::run_cases(&cases_root)?;
        let (_, failed) = validation::summarize(&records);

        if failed > 0 {
            eprintln!("\nValidation case failures:");
            for record in records
                .iter()
                .filter(|record| record.error.is_some() || !record.matched)
            {
                eprintln!("[FAIL] {}", validation::failure_message(record));
            }
            errors += failed;
        } else {
            println!("Validation cases: {} passed", records.len());
        }
    }

    println!("\nCheck complete: {} ok, {} errors", ok, errors);
    if errors > 0 {
        Err(format!("{} templates failed to render", errors))
    } else {
        Ok(())
    }
}

fn setup_project_files(project_path: &Path, config: &Config) -> io::Result<()> {
    fs::create_dir_all(project_path.join(&config.templates_path).join("pages"))?;
    fs::create_dir_all(project_path.join(&config.templates_path).join("layouts"))?;
    fs::create_dir_all(project_path.join(&config.templates_path).join("components"))?;
    fs::create_dir_all(project_path.join(&config.endpoints_path).join("api"))?;
    fs::create_dir_all(project_path.join(&config.static_path).join("css"))?;
    fs::create_dir_all(project_path.join(&config.static_path).join("js"))?;
    fs::create_dir_all(project_path.join(&config.static_path).join("images"))?;

    let config_path = project_path.join("xrml.toml");
    if !config_path.exists() {
        fs::write(&config_path, assets::default_config(&config.site_name))?;
    }

    let templates_dir = project_path.join(&config.templates_path);

    let base_layout_path = templates_dir.join("layouts/base.hrml");
    if !base_layout_path.exists() {
        fs::write(base_layout_path, assets::BASE_LAYOUT)?;
    }

    let nav_path = templates_dir.join("components/nav.hrml");
    if !nav_path.exists() {
        fs::write(nav_path, assets::NAV)?;
    }

    let index_path = templates_dir.join("pages/index.hrml");
    if !index_path.exists() {
        fs::write(index_path, assets::INDEX_PAGE)?;
    }

    let about_path = templates_dir.join("pages/about.hrml");
    if !about_path.exists() {
        fs::write(about_path, assets::ABOUT_PAGE)?;
    }

    let css_path = project_path.join(&config.static_path).join("css/style.css");
    if !css_path.exists() {
        fs::write(css_path, assets::STYLE_CSS)?;
    }

    let hello_path = project_path
        .join(&config.endpoints_path)
        .join("api/hello.hrml");
    if !hello_path.exists() {
        fs::write(hello_path, assets::HELLO_ENDPOINT)?;
    }

    let readme_path = project_path.join("README.md");
    if !readme_path.exists() {
        fs::write(readme_path, assets::readme(&config.site_name))?;
    }

    let gitignore_path = project_path.join(".gitignore");
    if !gitignore_path.exists() {
        fs::write(gitignore_path, assets::GITIGNORE)?;
    }

    Ok(())
}