maslc 1.0.1

Maduka Authorization Specification Language (MASL) toolchain and runtime
use std::fs;
use std::path::{Path, PathBuf};
use std::process::exit;

use masl_hir::document::Expr;

use crate::diagnostics::print_diagnostics;

/// Formats a MASL HIR expression as a human-readable Markdown string.
fn format_expr(expr: &Expr) -> String {
    match expr {
        Expr::Relation(r) => format!("`{}`", r),
        Expr::Traversal { steps, .. } => format!("`{}`", steps.join("::")),
        Expr::Not { operand, .. } => format!("`!{}`", format_expr(operand)),
        Expr::Union { operands, .. } => operands
            .iter()
            .map(format_expr)
            .collect::<Vec<_>>()
            .join(" \\| "),
        Expr::Intersection { operands, .. } => operands
            .iter()
            .map(format_expr)
            .collect::<Vec<_>>()
            .join(" & "),
        Expr::Difference { left, right, .. } => {
            format!("{} - {}", format_expr(left), format_expr(right))
        }
    }
}

/// Generates Markdown documentation for a single `.mdk` file.
pub fn doc_file(file: &Path, output_dir: Option<&Path>) -> Result<(), ()> {
    let source = match fs::read_to_string(file) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("Error reading file {}: {}", file.display(), e);
            return Err(());
        }
    };

    let ast = match masl_parser::parse(&source) {
        Ok(a) => a,
        Err(e) => {
            print_diagnostics(&e, &source, file.to_str().unwrap_or(""));
            return Err(());
        }
    };

    let hir = match masl_hir::analyze(ast) {
        Ok(h) => h,
        Err(e) => {
            print_diagnostics(&e, &source, file.to_str().unwrap_or(""));
            return Err(());
        }
    };

    let md = build_markdown(&hir);

    let out_dir = output_dir.unwrap_or(&PathBuf::from("./docs")).to_path_buf();
    if let Err(e) = fs::create_dir_all(&out_dir) {
        eprintln!(
            "Error creating output directory {}: {}",
            out_dir.display(),
            e
        );
        return Err(());
    }

    let out_file = out_dir.join(format!("{}.md", hir.namespace));
    if let Err(e) = fs::write(&out_file, md) {
        eprintln!(
            "Error writing documentation file {}: {}",
            out_file.display(),
            e
        );
        return Err(());
    }

    println!(
        "Successfully generated documentation at {}",
        out_file.display()
    );
    Ok(())
}

fn build_markdown(hir: &masl_hir::document::HirDocument) -> String {
    let mut md = String::new();
    md.push_str(&format!("# Namespace: `{}`\n\n", hir.namespace));
    md.push_str(&format!("**Schema Version:** `{}`\n\n", hir.schema_version));
    md.push_str("Generated automatically by `maslc doc`.\n\n---\n\n");

    if !hir.subjects.is_empty() {
        md.push_str("## Subjects\n\n");
        for (name, subject) in &hir.subjects {
            md.push_str(&format!("### Subject: `{}`\n\n", name));
            if !subject.relations.is_empty() {
                md.push_str("| Relation | Allowed Types |\n");
                md.push_str("| :--- | :--- |\n");
                for (rel_name, rel) in &subject.relations {
                    let types = format_allowed_types(rel);
                    md.push_str(&format!("| `{}` | {} |\n", rel_name, types));
                }
                md.push('\n');
            } else {
                md.push_str("*No relations declared.*\n\n");
            }
        }
        md.push_str("---\n\n");
    }

    if !hir.resources.is_empty() {
        md.push_str("## Resources\n\n");
        for (name, resource) in &hir.resources {
            md.push_str(&format!("### Resource: `{}`\n\n", name));

            md.push_str("#### Relations\n\n");
            if !resource.relations.is_empty() {
                md.push_str("| Relation | Allowed Types |\n");
                md.push_str("| :--- | :--- |\n");
                for (rel_name, rel) in &resource.relations {
                    let types = format_allowed_types(rel);
                    md.push_str(&format!("| `{}` | {} |\n", rel_name, types));
                }
                md.push('\n');
            } else {
                md.push_str("*No relations declared.*\n\n");
            }

            md.push_str("#### Grants (Permissions)\n\n");
            if !resource.grants.is_empty() {
                md.push_str("| Grant | Expression |\n");
                md.push_str("| :--- | :--- |\n");
                for (grant_name, grant) in &resource.grants {
                    md.push_str(&format!(
                        "| `{}` | {} |\n",
                        grant_name,
                        format_expr(&grant.expression)
                    ));
                }
                md.push('\n');
            } else {
                md.push_str("*No grants declared.*\n\n");
            }
        }
        md.push_str("---\n\n");
    }

    if !hir.aliases.is_empty() {
        md.push_str("## Aliases\n\n");
        md.push_str("| Alias | Expression |\n");
        md.push_str("| :--- | :--- |\n");
        for (name, expr) in &hir.aliases {
            md.push_str(&format!("| `{}` | {} |\n", name, format_expr(expr)));
        }
        md.push('\n');
    }

    md
}

fn format_allowed_types(rel: &masl_hir::document::Relation) -> String {
    rel.types
        .iter()
        .map(|t| match &t.relation {
            Some(r) => format!("`{}::{}`", t.target_name, r),
            None => format!("`{}`", t.target_name),
        })
        .collect::<Vec<_>>()
        .join(" \\| ")
}

/// Runs the `doc` command over a file or directory.
pub fn run(path: PathBuf, output: Option<PathBuf>) {
    if path.is_dir() {
        let mut success = true;
        let read_dir = match fs::read_dir(&path) {
            Ok(rd) => rd,
            Err(e) => {
                eprintln!("Error reading directory {}: {}", path.display(), e);
                exit(1);
            }
        };
        for entry in read_dir {
            let file_path = entry.unwrap().path();
            if file_path.is_file()
                && file_path.extension().is_some_and(|ext| ext == "mdk")
                && doc_file(&file_path, output.as_deref()).is_err()
            {
                success = false;
            }
        }
        if !success {
            exit(1);
        }
    } else if doc_file(&path, output.as_deref()).is_err() {
        exit(1);
    }
}