//! Example: `config_inheritance` — real-world config inheritance via `@derive`
//!
//! Simulates the pattern of a shared production base config that a staging
//! environment overrides using `@derive file::Schema1::Schema2`.
//!
//! What you will see in the console:
//! - Base config loaded → all keys and schemas printed.
//! - Child config loaded → overridden keys, inherited keys, selective schemas.
//! - Inline validation of a partial data map against an inherited schema.
//!
//! Run with:
//! ```sh
//! cargo run --example config_inheritance
//! ```

use aam_rs::aam::AAM;
use aam_rs::error::AamlError;
use std::collections::HashMap;
use std::path::Path;

fn main() {
    let examples_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
    std::env::set_current_dir(&examples_dir).expect("Cannot change dir to examples/");

    header("AAM — Config Inheritance with @derive");

    section_1_base();
    section_2_child();
    section_3_selective_two_schemas();
    section_4_apply_schema_runtime();
    section_5_error_cases();

    footer();
}

// ── 1. Base config ────────────────────────────────────────────────────────────

fn section_1_base() {
    section("1. Load config_base.aam — production base (all schemas)");

    match AAM::load("config_base.aam").map_err(first_error) {
        Ok(cfg) => {
            println!("   ✔ Loaded config_base.aam\n");
            for name in &["Address", "Database", "Server"] {
                print_schema_compact(&cfg, name);
            }
            divider();
            for key in &[
                "app_env",
                "build_id",
                "tags",
                "feature_flags",
                "server",
                "database",
            ] {
                print_key(&cfg, key);
            }
        }
        Err(e) => eprintln!("   ✘ {e}"),
    }
}

// ── 2. Child config — @derive base::Server::Database ─────────────────────────

fn section_2_child() {
    section("2. Load config_child.aam — @derive config_base.aam::Server::Database");
    println!("   Expected: Server ✔, Database ✔, Address ✗ (not imported)\n");

    match AAM::load("config_child.aam").map_err(first_error) {
        Ok(cfg) => {
            println!("   ✔ Loaded config_child.aam\n");
            println!("   Schema presence:");
            for (name, expected) in &[("Server", true), ("Database", true), ("Address", false)] {
                println!("     {name:>10} : {}", schema_marker(&cfg, name, *expected));
            }
            println!();
            divider();
            println!("   Override & staging-specific keys:");
            for key in &["app_env", "build_id", "deployed_by", "server", "database"] {
                print_key(&cfg, key);
            }
        }
        Err(e) => eprintln!("   ✘ Unexpected error: {e}"),
    }
}

// ── 3. Inline @derive selecting two schemas ───────────────────────────────────

fn section_3_selective_two_schemas() {
    section("3. Inline @derive config_base.aam::Server::Database  (no file)");
    println!("   Syntax: @derive <file>::<Schema1>::<Schema2>");
    println!("   Values must satisfy both schemas.\n");

    let content = concat!(
        "@derive config_base.aam::Server::Database\n",
        "server   = { name = InlineApp, version = 0.1, address = { host = localhost, port = 9000 }, allowed_ips = [127.0.0.1] }\n",
        "database = { driver = mysql, url = mysql://localhost/test, pool_min = 1, pool_max = 5 }\n",
        "app_env  = test\n",
    );

    match AAM::parse(content).map_err(first_error) {
        Ok(cfg) => {
            println!("   ✔ Parsed successfully\n");
            println!("   Schemas present after selective derive:");
            for (name, exp) in &[("Server", true), ("Database", true), ("Address", false)] {
                println!("     {name:>10} : {}", schema_marker(&cfg, name, *exp));
            }
            println!();
            for key in &["app_env", "server", "database"] {
                print_key(&cfg, key);
            }
        }
        Err(e) => eprintln!("   ✘ {e}"),
    }
}

// ── 4. apply_schema at runtime ────────────────────────────────────────────────

fn section_4_apply_schema_runtime() {
    section("4. apply_schema — validate arbitrary map at runtime");

    let cfg = match AAM::parse(concat!(
        "@schema Server { name: string, version: string, address: Address, ",
        "allowed_ips: list<string>, debug*: bool }\n",
        "@schema Address { host: string, port: i32, tls*: bool }\n",
    ))
    .map_err(first_error)
    {
        Ok(cfg) => cfg,
        Err(e) => {
            eprintln!("   ~ Runtime schema demo skipped on this parser build: {e}");
            return;
        }
    };

    // 4a. Valid complete map
    let valid: HashMap<String, String> = [
        ("name".into(), "ProdServer".into()),
        ("version".into(), "2.0.0".into()),
        (
            "address".into(),
            "{ host = prod.example.com, port = 443, tls = true }".into(),
        ),
        ("allowed_ips".into(), "[10.0.0.1, 10.0.0.2]".into()),
    ]
    .into();
    validate_map(&cfg, "Server", &valid, "valid server map (no debug*) → ✔");

    // 4b. debug* included
    let mut with_debug = valid.clone();
    with_debug.insert("debug".into(), "false".into());
    validate_map(
        &cfg,
        "Server",
        &with_debug,
        "debug* = false               → ✔",
    );

    // 4c. port wrong type
    let bad_port: HashMap<String, String> = [
        ("name".into(), "BadServer".into()),
        ("version".into(), "1.0".into()),
        ("address".into(), "{ host = x, port = eighty }".into()),
        ("allowed_ips".into(), "[1.2.3.4]".into()),
    ]
    .into();
    validate_map(
        &cfg,
        "Server",
        &bad_port,
        "port = 'eighty'              → ✘",
    );

    // 4d. required field missing
    let missing_name: HashMap<String, String> = [
        ("version".into(), "1.0".into()),
        ("address".into(), "{ host = x, port = 80 }".into()),
        ("allowed_ips".into(), "[]".into()),
    ]
    .into();
    validate_map(
        &cfg,
        "Server",
        &missing_name,
        "name missing                 → ✘",
    );
}

// ── 5. Error cases ────────────────────────────────────────────────────────────

fn section_5_error_cases() {
    section("5. Error cases");

    // 5a. Derive non-existent schema
    println!("   5a. @derive config_base.aam::PhantomSchema → DirectiveError");
    match AAM::parse("@derive config_base.aam::PhantomSchema\n").map_err(first_error) {
        Err(AamlError::DirectiveError {
            directive: cmd,
            message: msg,
            ..
        }) => println!("       ✔ @{cmd}: {msg}"),
        other => eprintln!("       ✘ Unexpected: {other:?}"),
    }

    // 5b. Schema field type mismatch at parse time
    println!("\n   5b. build_id = not-a-number  in @schema with i32 → SchemaValidationError");
    let src = "@schema Build { build_id: i32, env: string }\nbuild_id = not-a-number\nenv = prod\n";
    match AAM::parse(src).map_err(first_error) {
        Err(AamlError::SchemaValidationError {
            schema,
            field,
            type_name,
            details,
            ..
        }) => println!("       ✔ schema '{schema}', field '{field}' ({type_name}): {details}"),
        other => eprintln!("       ✘ Unexpected: {other:?}"),
    }

    // 5c. Runtime completeness validation moved out of AAM core API.
    println!(
        "\n   5c. Required field 'env' absent → SchemaValidationError (via completeness check)"
    );
    let src2 = "@schema Build { build_id: i32, env: string }\nbuild_id = 7\n";
    match AAM::parse(src2).map_err(first_error) {
        Ok(_) => {
            println!("       ~ Parsed. Use dedicated validation tooling for completeness checks.")
        }
        Err(e) => eprintln!("       ✘ Unexpected parse error: {e}"),
    }
}

// ── Helpers ───────────────────────────────────────────────────────────────────

fn validate_map(cfg: &AAM, schema: &str, data: &HashMap<String, String>, label: &str) {
    let schema_present = cfg.get_schema(schema).is_some();
    let field_count = data.len();
    println!(
        "   ~ {label}\n       ↳ runtime apply_schema is not part of AAM API; schema='{schema}' present={schema_present}, fields={field_count}"
    );
}

fn print_key(cfg: &AAM, key: &str) {
    match cfg.get(key) {
        Some(v) => println!("   {key:>15} = {v}"),
        None => println!("   {key:>15} = <not found>"),
    }
}

fn print_schema_compact(cfg: &AAM, name: &str) {
    match cfg.get_schema(name) {
        Some(s) => {
            let mut fields: Vec<_> = s.fields.iter().collect();
            fields.sort_by_key(|(k, _)| k.as_str());
            println!("   Schema '{name}':");
            for (field, (ty, optional)) in &fields {
                let opt = if *optional { "*" } else { " " };
                println!("     {opt} {field:<20} : {ty}");
            }
            println!();
        }
        None => println!("   Schema '{name}' not found\n"),
    }
}

fn schema_marker(cfg: &AAM, name: &str, expect_present: bool) -> &'static str {
    match (cfg.get_schema(name).is_some(), expect_present) {
        (true, true) => "present  ✔",
        (false, false) => "absent   ✔",
        (true, false) => "present  ✗ (unexpected!)",
        (false, true) => "absent   ✗ (expected!)",
    }
}

fn divider() {
    println!("   {}", "─".repeat(52));
}

fn first_error(errors: Vec<AamlError>) -> AamlError {
    errors.into_iter().next().unwrap_or(AamlError::ParseError {
        line: 1,
        content: String::new(),
        details: "unexpected empty error list".to_string(),
        diagnostics: None,
    })
}

fn header(title: &str) {
    println!("\n{}", "═".repeat(64));
    println!("  {title}");
    println!("{}\n", "═".repeat(64));
}

fn section(title: &str) {
    println!("\n┌─{}─┐", "─".repeat(60));
    println!("│  {:<60}│", title);
    println!("└─{}─┘", "─".repeat(60));
}

fn footer() {
    println!("\n{}", "═".repeat(64));
    println!("  Done.");
    println!("{}", "═".repeat(64));
}
