//! Extension-dispatching loader for AILang source files.
//!
//! AILang has two file forms today: Form A (`.ail`, the LLM
//! authoring surface) and Form B (`.ail.json`, the canonical
//! JSON-AST). These loaders accept both: for `.ail` they read the
//! source text and parse it via [`crate::parse()`]; for `.ail.json`
//! they delegate to [`ailang_core::load_module`].
//!
//! These functions live in `ailang-surface` rather than
//! `ailang-core` because `ailang-core` cannot import `ailang-surface`
//! without creating a circular crate dependency. The cross-crate
//! injection point is [`ailang_core::workspace::load_modules_with`]
//! (DFS-only loader) composed with [`ailang_core::workspace::build_workspace`]
//! (validation + registry). Surface owns the prelude inject step
//! between the two; the implicit-imports list `&["prelude"]` is
//! supplied at the surface call site. Introduced in iter pd.2 (post-
//! ext-cli.1's `load_workspace_with` shim retirement).

use ailang_core::ast::Module;
use ailang_core::workspace::{Workspace, WorkspaceLoadError};
use std::path::Path;

/// pd.2 (`prelude-decouple` milestone): the prelude bytes are embedded
/// here at compile time. The single source of truth is
/// `examples/prelude.ail` (Form A). Pre-pd.2, the embed lived in
/// `ailang-core` and used `prelude.ail.json`; that path retired with
/// the loader-split.
pub const PRELUDE_AIL: &str = include_str!("../../../examples/prelude.ail");

/// pd.2: parse the embedded prelude bytes into a `Module`.
///
/// Panics on parse failure — the prelude is build-time-validated by
/// every test run, so a parse failure here is a build-correctness
/// bug, not a runtime concern.
///
/// Mirrors the semantics of the retired `ailang_core::workspace::load_prelude`
/// (which deserialised `prelude.ail.json` via `serde_json::from_str`);
/// equivalence between the two parse paths is pinned by
/// `crates/ailang-surface/tests/prelude_module_hash_pin.rs`'s
/// cross-form-identity preflight.
pub fn parse_prelude() -> Module {
    crate::parse(PRELUDE_AIL).expect("examples/prelude.ail must parse as a Module")
}

/// raw-buf.4: parse the embedded raw_buf kernel-tier base-extension
/// module bytes into a `Module`. Mirror of [`parse_prelude`].
///
/// Source-of-truth: `ailang_kernel::RAW_BUF_AIL`. Declares the
/// `RawBuf` TypeDef with `param-in (a Int Float Bool)` and the four
/// `(intrinsic)` ops; codegen supplies their bodies via the
/// `RawBuf_op__<T>` intercept registry.
///
/// Panics on parse failure — build-time-validated by every drift run.
pub fn parse_raw_buf() -> Module {
    crate::parse(ailang_kernel::RAW_BUF_AIL)
        .expect("ailang_kernel::RAW_BUF_AIL must parse as a Module")
}

/// feat-series-step1: parse the embedded `series` kernel-tier library-
/// extension module bytes into a `Module`. Mirror of [`parse_raw_buf`].
///
/// Source-of-truth: `ailang_kernel::SERIES_AIL`. Declares the `Series`
/// TypeDef with `param-in (a Int Float)` and the five real-bodied ops
/// `new`/`push`/`at`/`len`/`total_count`, built over the `raw_buf`
/// kernel module's `RawBuf.*` ops. Unlike raw_buf this is a PURE
/// AILang library extension — no intercepts, no `(intrinsic)` markers.
///
/// Panics on parse failure — build-time-validated by every drift run.
pub fn parse_series() -> Module {
    crate::parse(ailang_kernel::SERIES_AIL)
        .expect("ailang_kernel::SERIES_AIL must parse as a Module")
}

fn is_ail_source(path: &Path) -> bool {
    path.extension().and_then(|s| s.to_str()) == Some("ail")
}

/// Load a single module from either `.ail` or `.ail.json`.
///
/// Dispatches on file extension:
///
/// - `.ail` — read source text, parse via [`crate::parse()`], return
///   the in-memory [`Module`].
/// - anything else — delegate to [`ailang_core::load_module`] and
///   convert its `Error` into the corresponding
///   [`WorkspaceLoadError`] variant.
///
/// Errors are returned as [`WorkspaceLoadError`] so that single-
/// module callers and workspace callers can share the same error
/// type (and `workspace_error_to_diagnostic` in the binary can
/// route them through one channel).
pub fn load_module(path: &Path) -> Result<Module, WorkspaceLoadError> {
    if is_ail_source(path) {
        let src = std::fs::read_to_string(path).map_err(|e| WorkspaceLoadError::Io {
            path: path.to_path_buf(),
            source: e,
        })?;
        crate::parse(&src).map_err(|e| WorkspaceLoadError::SurfaceParse {
            path: path.to_path_buf(),
            message: format!("{e}"),
        })
    } else {
        match ailang_core::load_module(path) {
            Ok(m) => Ok(m),
            Err(ailang_core::Error::Io(e)) => Err(WorkspaceLoadError::Io {
                path: path.to_path_buf(),
                source: e,
            }),
            Err(e) => Err(WorkspaceLoadError::Schema {
                path: path.to_path_buf(),
                source: e,
            }),
        }
    }
}

/// Load a workspace from an entry path, accepting either `.ail` or
/// `.ail.json` for the entry and for every transitive import.
///
/// Import resolution prefers a sibling `<module>.ail` over
/// `<module>.ail.json`; this is the new precedence rule baked into
/// `core::workspace::visit`. Mixed-extension workspaces (entry is
/// `.ail`, some imports are `.ail.json`) are valid.
pub fn load_workspace(entry: &Path) -> Result<Workspace, WorkspaceLoadError> {
    let (entry_name, root_dir, mut modules) =
        ailang_core::workspace::load_modules_with(entry, load_module)?;

    // parse_prelude() injects the built-in prelude into the
    // workspace. The prelude module's source carries `(kernel)`, so
    // it surfaces in the kernel-tier auto-import set below.
    if modules.contains_key("prelude") {
        return Err(WorkspaceLoadError::ReservedModuleName {
            name: "prelude".to_string(),
        });
    }
    modules.insert("prelude".to_string(), parse_prelude());

    // parse_raw_buf() injects the raw_buf kernel-tier base-extension
    // module — RawBuf, the first real-payload kernel extension. Its
    // source carries `(kernel)`, so the kernel-flag filter below
    // auto-imports it.
    if modules.contains_key("raw_buf") {
        return Err(WorkspaceLoadError::ReservedModuleName {
            name: "raw_buf".to_string(),
        });
    }
    modules.insert("raw_buf".to_string(), parse_raw_buf());

    // parse_series() injects the series kernel-tier library-extension
    // module — Series, a bounded ring buffer over RawBuf. Pure AILang
    // (no intercepts); its body references the raw_buf kernel module,
    // which the kernel auto-import set below makes visible. Its source
    // carries `(kernel)`, so the kernel-flag filter auto-imports it.
    if modules.contains_key("series") {
        return Err(WorkspaceLoadError::ReservedModuleName {
            name: "series".to_string(),
        });
    }
    modules.insert("series".to_string(), parse_series());

    // Derive the implicit-imports list from `kernel: true` modules.
    // Replaces the previous hardcoded `&["prelude"]` literal: any
    // workspace-loaded module that carries the kernel flag is now
    // auto-imported. See prep.3 of the kernel-extension-mechanics
    // milestone.
    let kernel_names: Vec<String> = modules
        .values()
        .filter(|m| m.kernel)
        .map(|m| m.name.clone())
        .collect();
    let implicit_imports: Vec<&str> =
        kernel_names.iter().map(String::as_str).collect();

    ailang_core::workspace::build_workspace(
        entry_name,
        root_dir,
        modules,
        &implicit_imports,
    )
}
