use std::{fs, path::Path};

use crate::{
    ast::Program,
    diag::Diagnostic,
    lexer, lower,
    project::{self, ProjectArtifact, SourceFile, ToolFailure, WorkspaceArtifact},
    sexpr::{Atom, SExpr, SExprKind},
};

pub fn generate(input: &str, output_dir: &str) -> Result<(), ToolFailure> {
    let docs = if project::is_project_input(input) {
        let loaded = project::load_project_sources_for_tools(input)?;
        render_project(&loaded.artifact, &loaded.sources)?
    } else {
        let source = fs::read_to_string(input).map_err(|err| ToolFailure {
            diagnostics: vec![Diagnostic::new(
                input,
                "InputReadFailed",
                format!("cannot read `{}`: {}", input, err),
            )],
            sources: Vec::new(),
            artifact: None,
        })?;
        let sources = vec![SourceFile {
            path: input.to_string(),
            source,
        }];
        render_file(input, &sources)?
    };

    fs::create_dir_all(output_dir).map_err(|err| ToolFailure {
        diagnostics: vec![Diagnostic::new(
            output_dir,
            "OutputWriteFailed",
            format!(
                "cannot create documentation directory `{}`: {}",
                output_dir, err
            ),
        )],
        sources: Vec::new(),
        artifact: None,
    })?;
    fs::write(Path::new(output_dir).join("index.md"), docs).map_err(|err| ToolFailure {
        diagnostics: vec![Diagnostic::new(
            output_dir,
            "OutputWriteFailed",
            format!("cannot write documentation into `{}`: {}", output_dir, err),
        )],
        sources: Vec::new(),
        artifact: None,
    })
}

fn render_project(
    artifact: &ProjectArtifact,
    sources: &[SourceFile],
) -> Result<String, ToolFailure> {
    let mut out = String::new();
    out.push_str("# Project ");
    out.push_str(&artifact.project_name);
    out.push_str("\n\n");
    if let Some(workspace) = &artifact.workspace {
        render_workspace(&mut out, workspace);
    }
    for source in sources {
        let module = document_source(source)?;
        render_module(&mut out, &module);
    }
    Ok(out)
}

fn render_workspace(out: &mut String, workspace: &WorkspaceArtifact) {
    out.push_str("## Workspace\n\n");
    render_list(out, "Members", &workspace.members);

    let packages = workspace
        .packages
        .iter()
        .map(|package| {
            format!(
                "{} {} (entry {})",
                package.name, package.version, package.entry
            )
        })
        .collect::<Vec<_>>();
    render_list(out, "Packages", &packages);

    let dependencies = workspace
        .dependencies
        .iter()
        .map(|dependency| {
            format!(
                "{} -> {} ({}, {})",
                dependency.from, dependency.to, dependency.kind, dependency.path
            )
        })
        .collect::<Vec<_>>();
    render_list(out, "Package Dependencies", &dependencies);
}

fn render_file(title: &str, sources: &[SourceFile]) -> Result<String, ToolFailure> {
    let mut out = String::new();
    out.push_str("# File ");
    out.push_str(title);
    out.push_str("\n\n");
    let module = document_source(&sources[0])?;
    render_module(&mut out, &module);
    Ok(out)
}

fn document_source(source: &SourceFile) -> Result<DocModule, ToolFailure> {
    let tokens = lexer::lex(&source.path, &source.source).map_err(|diagnostics| ToolFailure {
        diagnostics,
        sources: vec![source.clone()],
        artifact: None,
    })?;
    let forms = crate::sexpr::parse(&source.path, &tokens).map_err(|diagnostics| ToolFailure {
        diagnostics,
        sources: vec![source.clone()],
        artifact: None,
    })?;
    let program = lower::lower_program(&source.path, &forms).ok();
    Ok(module_from_forms(&source.path, &forms, program.as_ref()))
}

struct DocModule {
    title: String,
    imports: Vec<String>,
    exports: Vec<String>,
    structs: Vec<String>,
    functions: Vec<String>,
    tests: Vec<String>,
}

fn module_from_forms(file: &str, forms: &[SExpr], program: Option<&Program>) -> DocModule {
    let mut module = DocModule {
        title: file.to_string(),
        imports: Vec::new(),
        exports: Vec::new(),
        structs: Vec::new(),
        functions: Vec::new(),
        tests: Vec::new(),
    };

    for form in forms {
        let Some(items) = list(form) else {
            continue;
        };
        match items.first().and_then(ident) {
            Some("module") => {
                if let Some(name) = items.get(1).and_then(ident) {
                    module.title = name.to_string();
                }
                if let Some(exports) = items.get(2).and_then(list) {
                    if matches!(exports.first().and_then(ident), Some("export")) {
                        module
                            .exports
                            .extend(exports[1..].iter().filter_map(ident).map(str::to_string));
                    }
                }
            }
            Some("import") => {
                if let Some(name) = items.get(1).and_then(ident) {
                    module.imports.push(name.to_string());
                }
            }
            Some("struct") => {
                if let Some(name) = items.get(1).and_then(ident) {
                    module.structs.push(name.to_string());
                }
            }
            Some("fn") => {
                if let Some(name) = items.get(1).and_then(ident) {
                    module.functions.push(name.to_string());
                }
            }
            Some("test") => {
                if let Some(name) = items.get(1).and_then(string_atom) {
                    module.tests.push(name.to_string());
                }
            }
            _ => {}
        }
    }

    if let Some(program) = program {
        module.title = program.module.clone();
        module.structs = program
            .structs
            .iter()
            .map(|decl| decl.name.clone())
            .collect();
        module.functions = program
            .functions
            .iter()
            .map(|function| {
                let params = function
                    .params
                    .iter()
                    .map(|param| format!("{} {}", param.name, param.ty))
                    .collect::<Vec<_>>()
                    .join(", ");
                format!("{}({}) -> {}", function.name, params, function.return_type)
            })
            .collect();
        module.tests = program.tests.iter().map(|test| test.name.clone()).collect();
    }

    module.imports.sort();
    module.exports.sort();
    module.structs.sort();
    module.functions.sort();
    module.tests.sort();
    module
}

fn render_module(out: &mut String, module: &DocModule) {
    out.push_str("## Module ");
    out.push_str(&module.title);
    out.push_str("\n\n");
    render_list(out, "Imports", &module.imports);
    render_list(out, "Exports", &module.exports);
    render_list(out, "Structs", &module.structs);
    render_list(out, "Functions", &module.functions);
    render_list(out, "Tests", &module.tests);
}

fn render_list(out: &mut String, title: &str, values: &[String]) {
    out.push_str("### ");
    out.push_str(title);
    out.push('\n');
    if values.is_empty() {
        out.push_str("\nNone.\n\n");
        return;
    }
    for value in values {
        out.push_str("- `");
        out.push_str(&value.replace('`', "\\`"));
        out.push_str("`\n");
    }
    out.push('\n');
}

fn list(expr: &SExpr) -> Option<&[SExpr]> {
    match &expr.kind {
        SExprKind::List(items) => Some(items),
        _ => None,
    }
}

fn ident(expr: &SExpr) -> Option<&str> {
    match &expr.kind {
        SExprKind::Atom(Atom::Ident(value)) => Some(value),
        _ => None,
    }
}

fn string_atom(expr: &SExpr) -> Option<&str> {
    match &expr.kind {
        SExprKind::Atom(Atom::String(value)) => Some(value),
        _ => None,
    }
}
