use std::{
    collections::{BTreeMap, BTreeSet},
    fs,
    path::Path,
};

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

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 modules = sources
        .iter()
        .map(document_source)
        .collect::<Result<Vec<_>, _>>()?;

    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);
        render_workspace_package_public_api(&mut out, workspace, &modules);
    } else {
        render_project_package_public_api(&mut out, artifact, &modules);
    }
    for module in &modules {
        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 lowerable_forms = forms
        .iter()
        .filter(|form| !matches!(list_head(form), Some("import")))
        .cloned()
        .collect::<Vec<_>>();
    let program = lower::lower_program(&source.path, &lowerable_forms).ok();
    Ok(module_from_forms(&source.path, &forms, program.as_ref()))
}

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

#[derive(Default)]
struct PublicApi {
    functions: Vec<DocFunction>,
    structs: Vec<DocStruct>,
    enums: Vec<DocEnum>,
}

struct DocFunction {
    name: String,
    signature: String,
}

struct DocStruct {
    name: String,
    fields: Vec<String>,
}

struct DocEnum {
    name: String,
    variants: Vec<String>,
}

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

    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.public_api = public_api_from_program(program, &module.exports);
    }

    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_public_api_section(out, "###", &module.public_api);
    render_list(out, "Structs", &module.structs);
    render_list(out, "Functions", &module.functions);
    render_list(out, "Tests", &module.tests);
}

fn render_project_package_public_api(
    out: &mut String,
    artifact: &ProjectArtifact,
    modules: &[DocModule],
) {
    let local_root = Path::new(&artifact.project_root).join(&artifact.source_root);
    let local_modules = modules
        .iter()
        .filter(|module| Path::new(&module.path).starts_with(&local_root))
        .collect::<Vec<_>>();
    render_package_public_api(out, &artifact.project_name, &local_modules);
}

fn render_workspace_package_public_api(
    out: &mut String,
    workspace: &WorkspaceArtifact,
    modules: &[DocModule],
) {
    let by_path = modules
        .iter()
        .map(|module| (module.path.as_str(), module))
        .collect::<BTreeMap<_, _>>();

    for package in &workspace.packages {
        let local_root = Path::new(&package.root).join(&package.source_root);
        let package_modules = package
            .modules
            .iter()
            .filter(|module| Path::new(&module.path).starts_with(&local_root))
            .filter_map(|module| by_path.get(module.path.as_str()).copied())
            .collect::<Vec<_>>();
        render_package_public_api(
            out,
            &format!("{} {}", package.name, package.version),
            &package_modules,
        );
    }
}

fn render_package_public_api(out: &mut String, package_name: &str, modules: &[&DocModule]) {
    out.push_str("## Package API ");
    out.push_str(package_name);
    out.push_str("\n\n");

    let public_modules = modules
        .iter()
        .copied()
        .filter(|module| !module.public_api.is_empty())
        .collect::<Vec<_>>();
    if public_modules.is_empty() {
        out.push_str("None.\n\n");
        return;
    }

    for module in public_modules {
        out.push_str("### Module ");
        out.push_str(&module.title);
        out.push_str("\n\n");
        render_public_api_body(out, "####", &module.public_api);
    }
}

fn render_public_api_section(out: &mut String, heading: &str, public_api: &PublicApi) {
    out.push_str(heading);
    out.push_str(" Public API\n\n");
    render_public_api_body(out, "####", public_api);
}

fn render_public_api_body(out: &mut String, heading: &str, public_api: &PublicApi) {
    if public_api.is_empty() {
        out.push_str("None.\n\n");
        return;
    }

    if !public_api.functions.is_empty() {
        out.push_str(heading);
        out.push_str(" Functions\n");
        for function in &public_api.functions {
            render_code_bullet(out, &function.signature);
        }
        out.push('\n');
    }

    if !public_api.structs.is_empty() {
        out.push_str(heading);
        out.push_str(" Structs\n");
        for struct_decl in &public_api.structs {
            render_code_bullet(out, &format!("struct {}", struct_decl.name));
            for field in &struct_decl.fields {
                render_indented_code_bullet(out, field);
            }
        }
        out.push('\n');
    }

    if !public_api.enums.is_empty() {
        out.push_str(heading);
        out.push_str(" Enums\n");
        for enum_decl in &public_api.enums {
            render_code_bullet(out, &format!("enum {}", enum_decl.name));
            for variant in &enum_decl.variants {
                render_indented_code_bullet(out, variant);
            }
        }
        out.push('\n');
    }
}

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_head(expr: &SExpr) -> Option<&str> {
    list(expr).and_then(|items| items.first()).and_then(ident)
}

fn render_code_bullet(out: &mut String, value: &str) {
    out.push_str("- `");
    out.push_str(&value.replace('`', "\\`"));
    out.push_str("`\n");
}

fn render_indented_code_bullet(out: &mut String, value: &str) {
    out.push_str("  - `");
    out.push_str(&value.replace('`', "\\`"));
    out.push_str("`\n");
}

impl PublicApi {
    fn is_empty(&self) -> bool {
        self.functions.is_empty() && self.structs.is_empty() && self.enums.is_empty()
    }
}

fn public_api_from_program(program: &Program, exports: &[String]) -> PublicApi {
    let export_names = exports.iter().cloned().collect::<BTreeSet<_>>();
    let aliases = alias_targets(program);

    let mut functions = program
        .functions
        .iter()
        .filter(|function| export_names.contains(&function.name))
        .map(|function| DocFunction {
            name: function.name.clone(),
            signature: function_signature(
                &function.name,
                function
                    .params
                    .iter()
                    .map(|param| (param.name.as_str(), &param.ty)),
                &function.return_type,
                &aliases,
            ),
        })
        .collect::<Vec<_>>();
    functions.sort_by(|left, right| left.name.cmp(&right.name));

    let mut structs = program
        .structs
        .iter()
        .filter(|struct_decl| export_names.contains(&struct_decl.name))
        .map(|struct_decl| DocStruct {
            name: struct_decl.name.clone(),
            fields: struct_decl
                .fields
                .iter()
                .map(|field| {
                    format!(
                        "{}: {}",
                        field.name,
                        display_public_type(&field.ty, &aliases)
                    )
                })
                .collect(),
        })
        .collect::<Vec<_>>();
    structs.sort_by(|left, right| left.name.cmp(&right.name));

    let mut enums = program
        .enums
        .iter()
        .filter(|enum_decl| export_names.contains(&enum_decl.name))
        .map(|enum_decl| DocEnum {
            name: enum_decl.name.clone(),
            variants: enum_decl
                .variants
                .iter()
                .map(|variant| match &variant.payload_ty {
                    Some(payload_ty) => {
                        format!(
                            "{}({})",
                            variant.name,
                            display_public_type(payload_ty, &aliases)
                        )
                    }
                    None => variant.name.clone(),
                })
                .collect(),
        })
        .collect::<Vec<_>>();
    enums.sort_by(|left, right| left.name.cmp(&right.name));

    PublicApi {
        functions,
        structs,
        enums,
    }
}

fn function_signature<'a>(
    name: &str,
    params: impl Iterator<Item = (&'a str, &'a Type)>,
    return_type: &Type,
    aliases: &BTreeMap<String, Type>,
) -> String {
    let params = params
        .map(|(name, ty)| format!("{}: {}", name, display_public_type(ty, aliases)))
        .collect::<Vec<_>>()
        .join(", ");
    format!(
        "fn {}({}) -> {}",
        name,
        params,
        display_public_type(return_type, aliases)
    )
}

fn alias_targets(program: &Program) -> BTreeMap<String, Type> {
    let raw = program
        .type_aliases
        .iter()
        .map(|alias| (alias.name.clone(), alias.target.clone()))
        .collect::<BTreeMap<_, _>>();
    raw.keys()
        .map(|name| {
            let mut visiting = BTreeSet::new();
            (
                name.clone(),
                resolve_alias_type(&Type::Named(name.clone()), &raw, &mut visiting),
            )
        })
        .collect()
}

fn display_public_type(ty: &Type, aliases: &BTreeMap<String, Type>) -> String {
    let mut visiting = BTreeSet::new();
    resolve_alias_type(ty, aliases, &mut visiting).to_string()
}

fn resolve_alias_type(
    ty: &Type,
    aliases: &BTreeMap<String, Type>,
    visiting: &mut BTreeSet<String>,
) -> Type {
    match ty {
        Type::Named(name) => {
            let Some(target) = aliases.get(name) else {
                return ty.clone();
            };
            if !visiting.insert(name.clone()) {
                return ty.clone();
            }
            let resolved = resolve_alias_type(target, aliases, visiting);
            visiting.remove(name);
            resolved
        }
        Type::Ptr(inner) => Type::Ptr(Box::new(resolve_alias_type(inner, aliases, visiting))),
        Type::Array(inner, len) => {
            Type::Array(Box::new(resolve_alias_type(inner, aliases, visiting)), *len)
        }
        Type::Vec(inner) => Type::Vec(Box::new(resolve_alias_type(inner, aliases, visiting))),
        Type::Slice(inner) => Type::Slice(Box::new(resolve_alias_type(inner, aliases, visiting))),
        Type::Option(inner) => Type::Option(Box::new(resolve_alias_type(inner, aliases, visiting))),
        Type::Result(ok, err) => Type::Result(
            Box::new(resolve_alias_type(ok, aliases, visiting)),
            Box::new(resolve_alias_type(err, aliases, visiting)),
        ),
        Type::I32
        | Type::I64
        | Type::U32
        | Type::U64
        | Type::F64
        | Type::Bool
        | Type::Unit
        | Type::String => ty.clone(),
    }
}

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,
    }
}
