#!/usr/bin/env nu

# NCL → index.json Builder
#
# Exports each {type}/{lang}/_index.ncl via `nickel export` and writes
# the result to the server content output dirs with the expected envelope:
#
#   { generated_at, content_type, language, posts: [...] }
#
# Output locations (both written when they exist):
#   $SITE_SERVER_CONTENT_ROOT/{type}/{lang}/index.json  (SSR filesystem read)
#   $SITE_PUBLIC_PATH/r/{type}/{lang}/index.json        (static asset for WASM)
#
# Usage:
#   nu build-ncl-json.nu [--content-dir PATH] [--output-dir PATH]
#                        [--public-dir PATH] [--type TYPE] [--lang LANG]
#                        [--dry-run] [--verbose]
#
# Environment variables (override defaults):
#   SITE_CONTENT_PATH         — content root       (default: site/content)
#   SITE_SERVER_CONTENT_ROOT  — SSR output root    (default: target/site/r)
#   SITE_PUBLIC_PATH          — public assets root (default: public)

# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main [
    --content-dir: string = ""   # Content root
    --output-dir:  string = ""   # SSR output root  (target/site/r)
    --public-dir:  string = ""   # Public assets dir (public)
    --type: string = ""          # Filter to one content type
    --lang: string = ""          # Filter to one language
    --dry-run                    # Print what would be written without writing
    --verbose                    # Verbose output per post
] {
    let content_root = resolve_env "SITE_CONTENT_PATH" $content_dir "site/content"
    let ssr_root     = resolve_env "SITE_SERVER_CONTENT_ROOT" $output_dir "target/site/r"
    let public_root  = resolve_env "SITE_PUBLIC_PATH" $public_dir "public"

    if not ($content_root | path exists) {
        error make { msg: $"Content directory not found: ($content_root)" }
    }

    print $"(ansi cyan)NCL → JSON Builder(ansi reset)"
    print $"  content : ($content_root)"
    print $"  ssr-out : ($ssr_root)"
    print $"  pub-out : ($public_root)/r"
    if $dry_run { print $"(ansi yellow)[dry-run](ansi reset)" }

    let types = discover_dirs $content_root $type
    if ($types | is-empty) { print "No content types found."; return }

    mut total_written  = 0
    mut total_skipped  = 0
    mut total_errors   = 0

    for ct in $types {
        let langs = discover_dirs $"($content_root)/($ct)" $lang
        for language in $langs {
            let result = export_lang $content_root $ct $language $ssr_root $public_root $dry_run $verbose
            $total_written = $total_written + $result.written
            $total_skipped = $total_skipped + $result.skipped
            $total_errors  = $total_errors  + $result.errors
        }
    }

    print ""
    print $"(ansi green)Done(ansi reset) — written: ($total_written)  skipped: ($total_skipped)  errors: ($total_errors)"

    if $total_errors > 0 { exit 1 }
}

# ---------------------------------------------------------------------------
# Per-language export
# ---------------------------------------------------------------------------

def export_lang [
    content_root: string, ct: string, lang: string,
    ssr_root: string, public_root: string,
    dry_run: bool, verbose: bool
] {
    let ncl_index = $"($content_root)/($ct)/($lang)/_index.ncl"

    if not ($ncl_index | path exists) {
        if $verbose { print $"  skip  ($ct)/($lang): no _index.ncl" }
        return { written: 0, skipped: 1, errors: 0 }
    }

    print $"  (ansi blue)($ct)/($lang)(ansi reset)"

    # Export NCL → raw JSON string
    let json_str = try {
        nickel export $ncl_index --format json
    } catch { |e|
        print $"    (ansi red)FAIL(ansi reset) nickel export: ($e.msg | str trim)"
        return { written: 0, skipped: 0, errors: 1 }
    }

    # Parse and validate we got an array
    let posts_raw = try {
        $json_str | from json
    } catch { |e|
        print $"    (ansi red)FAIL(ansi reset) JSON parse: ($e.msg | str trim)"
        return { written: 0, skipped: 0, errors: 1 }
    }

    let type_name = $posts_raw | describe
    if not ($type_name | str starts-with "table") and not ($type_name | str starts-with "list") {
        print $"    (ansi red)FAIL(ansi reset) expected JSON array, got ($type_name)"
        return { written: 0, skipped: 0, errors: 1 }
    }

    let post_count = $posts_raw | length
    if $verbose { print $"    ($post_count) posts" }

    # Build server envelope
    let now = (date now | format date "%Y-%m-%dT%H:%M:%S%.6fZ")
    let envelope = {
        generated_at:  $now,
        content_type:  $ct,
        language:      $lang,
        posts:         $posts_raw,
    }

    let json_out = $envelope | to json --indent 2

    let ssr_path    = $"($ssr_root)/($ct)/($lang)/index.json"
    let public_path = $"($public_root)/r/($ct)/($lang)/index.json"

    if $dry_run {
        print $"    [dry-run] → ($ssr_path)"
        print $"    [dry-run] → ($public_path)"
        print $"    [dry-run] → ($public_path | str replace 'index.json' 'filter-index.json')"
        return { written: 1, skipped: 0, errors: 0 }
    }

    let wrote_ssr = write_mkdir $ssr_path $json_out $verbose
    let wrote_pub = write_mkdir $public_path $json_out $verbose

    # Generate filter-index.json (categories + tags with counts)
    let metadata_file = $"($content_root)/($ct)/($ct).ncl"
    if ($metadata_file | path exists) {
        let metadata = try {
            nickel export $metadata_file --format json | from json
        } catch { |e|
            if $verbose { print $"    ⚠️ Could not load metadata: ($e.msg | str trim)" }
            null
        }

        if ($metadata != null) {
            # Build categories as a record (HashMap) — key = category id, value = {emoji, count}
            # FilterIndex expects HashMap<String, CategoryData> not an array.
            let categories = (
                $metadata.categories
                | reduce -f {} { |cat, acc|
                    let cat_emoji = if ($cat.emoji? | is-empty) { "" } else { $cat.emoji }
                    let count = ($posts_raw | where { |row| ($row.category? | default "") == $cat.id } | length)
                    $acc | insert $cat.id { emoji: $cat_emoji, count: $count }
                }
            )

            # Build tags as a record (HashMap) — key = tag id, value = {emoji, count}
            let tags = (
                $metadata.tags
                | reduce -f {} { |tag, acc|
                    let tag_emoji = if ($tag.emoji? | is-empty) { "" } else { $tag.emoji }
                    let count = ($posts_raw | where { |row| ($row.tags? | default [] | any { |t| $t == $tag.id }) } | length)
                    $acc | insert $tag.id { emoji: $tag_emoji, count: $count }
                }
            )

            # Create filter-index envelope — categories/tags as objects (HashMap) for FilterIndex serde
            let filter_envelope = {
                generated_at: $now
                content_type: $ct
                language: $lang
                total_posts: ($posts_raw | length)
                categories: $categories
                tags: $tags
            }

            let filter_json = $filter_envelope | to json --indent 2
            let filter_ssr_path = $"($ssr_root)/($ct)/($lang)/filter-index.json"
            let filter_public_path = $"($public_root)/r/($ct)/($lang)/filter-index.json"

            let _wrote_filter_ssr = write_mkdir $filter_ssr_path $filter_json $verbose
            let _wrote_filter_pub = write_mkdir $filter_public_path $filter_json $verbose
        }
    }

    if $wrote_ssr or $wrote_pub {
        print $"    (ansi green)ok(ansi reset)  ($post_count) posts + filter-index"
        { written: 1, skipped: 0, errors: 0 }
    } else {
        print $"    (ansi yellow)skip(ansi reset) output dirs not found"
        { written: 0, skipped: 1, errors: 0 }
    }
}

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def resolve_env [var_name: string, cli_arg: string, fallback: string] {
    if $cli_arg != "" { return $cli_arg }
    try { $env | get $var_name } catch { $fallback }
}

def discover_dirs [parent: string, filter: string] {
    if not ($parent | path exists) { return [] }
    let all = (ls $parent)
        | where type == "dir"
        | get name
        | each { |p| $p | path basename }
        | where { |d| not ($d | str starts-with ".") }
    if $filter != "" { $all | where { |d| $d == $filter } } else { $all }
}

# Create parent dirs if grandparent exists, then write file.
# Returns true if written, false if grandparent (output root) doesn't exist.
def write_mkdir [path: string, content: string, verbose: bool] {
    let parent      = $path | path dirname
    let grandparent = $parent | path dirname
    if not ($grandparent | path exists) {
        if $verbose { print $"    skip \(root not found\): ($grandparent)" }
        return false
    }
    mkdir $parent
    $content | save --force $path
    true
}
