# ━━━ store.ling — the temple: repo layout, adaptive object store, altar ━━━
# A repository is a temple (.miao). Every version of a file is a spirit,
# identified by the BLAKE3 hash of its full content. HOW a spirit is
# enshrined is decided per-version by the diviner (sx_spirit_put):
#   whole  — small spirits, stored entire
#   delta  — text spirits, stored as a line-delta on the previous version
#            (chains bounded by depth so summoning stays fast)
#   chunks — giant spirits, split by content-defined chunking; chunks
#            dedup across versions AND across files
# Payloads live in relics/pay/<blake3>, raw bytes on disk.
#
# fns prefixed sx_.

# ── repo discovery ────────────────────────────────────────────────────
fn sx_root() {
    bind cached = st_get("sx_root", "")
    if cached != "" { return cached }
    bind d = ut_norm(absolute_path("."))
    bind i = 0.0
    while i < 48.0 {
        if file_exists(d + "/.miao/MIAO") {
            st_set("sx_root", d)
            return d
        }
        bind p = ut_parent(d)
        if p == d { bind i = 48.0 }
        bind d = p
        bind i = i + 1.0
    }
    return ""
}

fn sx_dir() { return sx_root() + "/.miao" }

# relative path of cwd inside the repo ("" at root)
fn sx_prefix() {
    bind root = sx_root()
    bind cwd = ut_norm(absolute_path("."))
    if cwd == root { return "" }
    bind rel = str_strip_prefix(cwd, root + "/")
    if rel == cwd { return "" }
    return rel
}

# ── consecration (init) ───────────────────────────────────────────────
fn sx_init(path) {
    bind base = ut_norm(path)
    bind d = base + "/.miao"
    if file_exists(d + "/MIAO") { return "exists" }
    make_dir(d + "/relics/spirit")
    make_dir(d + "/relics/pay")
    make_dir(d + "/relics/rite")
    make_dir(d + "/souls")
    make_dir(d + "/talismans")
    make_dir(d + "/tmp")
    write_file(d + "/MIAO", "miao 1\n")
    write_file(d + "/SOUL", "soul: main\n")
    write_file(d + "/souls/main", "")
    write_file(d + "/altar", "")
    return "ok"
}

# ── config ────────────────────────────────────────────────────────────
fn sx_cfg_get(key, def) {
    bind lines = ut_read_lines(sx_dir() + "/config")
    bind i = 0.0
    while i < len(lines) {
        bind ln = list_get(lines, i)
        if str_starts_with(ln, key + "|") { return str_slice(ln, len(key) + 1.0, 99999.0) }
        bind i = i + 1.0
    }
    return def
}

fn sx_cfg_set(key, val) {
    bind lines = ut_read_lines(sx_dir() + "/config")
    bind out = ""
    bind i = 0.0
    while i < len(lines) {
        bind ln = list_get(lines, i)
        if str_starts_with(ln, key + "|") == false { bind out = out + ln + "\n" }
        bind i = i + 1.0
    }
    write_file(sx_dir() + "/config", out + key + "|" + val + "\n")
    return 0.0
}

fn sx_author() {
    bind a = sx_cfg_get("author", "")
    if a != "" { return a }
    bind u = env_get("USERNAME", "")
    if u != "" { return u }
    bind u2 = env_get("USER", "")
    if u2 != "" { return u2 }
    return "keeper"
}

# ── the living soul (HEAD) ────────────────────────────────────────────
# returns [type, value]: ["soul", name] or ["rite", id]
fn sx_head() {
    bind t = str_trim(ut_read_text(sx_dir() + "/SOUL"))
    if str_starts_with(t, "soul: ") { return ["soul", str_slice(t, 6.0, 99999.0)] }
    if str_starts_with(t, "rite: ") { return ["rite", str_slice(t, 6.0, 99999.0)] }
    return ["soul", "main"]
}

fn sx_set_head_soul(name) {
    write_file(sx_dir() + "/SOUL", "soul: " + name + "\n")
    return 0.0
}

fn sx_set_head_rite(id) {
    write_file(sx_dir() + "/SOUL", "rite: " + id + "\n")
    return 0.0
}

fn sx_tip(soul) { return str_trim(ut_read_text(sx_dir() + "/souls/" + soul)) }

fn sx_set_tip(soul, id) {
    write_file(sx_dir() + "/souls/" + soul, id + "\n")
    return 0.0
}

fn sx_souls() {
    bind es = list_dir(sx_dir() + "/souls")
    bind out = list_new()
    bind i = 0.0
    while i < len(es) {
        bind out = list_push(out, path_name(list_get(es, i)))
        bind i = i + 1.0
    }
    return ut_sort(out)
}

# the rite id the working tree sits on ("" when the temple is newborn)
fn sx_head_rite() {
    bind h = sx_head()
    if list_get(h, 0.0) == "rite" { return list_get(h, 1.0) }
    return sx_tip(list_get(h, 1.0))
}

# ── talismans (tags) ──────────────────────────────────────────────────
fn sx_talisman_set(name, rite) {
    write_file(sx_dir() + "/talismans/" + name, rite + "\n")
    return 0.0
}

fn sx_talisman_get(name) { return str_trim(ut_read_text(sx_dir() + "/talismans/" + name)) }

fn sx_talismans() {
    bind es = list_dir(sx_dir() + "/talismans")
    bind out = list_new()
    bind i = 0.0
    while i < len(es) {
        bind out = list_push(out, path_name(list_get(es, i)))
        bind i = i + 1.0
    }
    return ut_sort(out)
}

# resolve a name: soul | talisman | rite id (full or >=7 char prefix) -> rite id or ""
fn sx_resolve(name) {
    if name == "" { return sx_head_rite() }
    if file_exists(sx_dir() + "/souls/" + name) { return sx_tip(name) }
    if file_exists(sx_dir() + "/talismans/" + name) { return sx_talisman_get(name) }
    if file_exists(sx_dir() + "/relics/rite/" + name) { return name }
    # prefix search
    if len(name) >= 7.0 {
        bind es = list_dir(sx_dir() + "/relics/rite")
        bind i = 0.0
        bind found = ""
        while i < len(es) {
            bind id = path_name(list_get(es, i))
            if str_starts_with(id, name) {
                if found != "" { return "" }
                bind found = id
            }
            bind i = i + 1.0
        }
        return found
    }
    return ""
}

# ── payload objects (raw bytes, blake3-named) ─────────────────────────
fn sx_pay_put_hex(hex) {
    bind tmp = sx_dir() + "/tmp/pay"
    file_write_hex(tmp, hex)
    bind id = file_hash(tmp)
    bind dest = sx_dir() + "/relics/pay/" + id
    if file_exists(dest) {
        remove_file(tmp)
    } else {
        rename_file(tmp, dest)
    }
    return id
}

fn sx_pay_put_text(text) {
    bind id = hash_hex(text)
    bind dest = sx_dir() + "/relics/pay/" + id
    if file_exists(dest) == false { write_file(dest, text) }
    return id
}

fn sx_pay_hex(id) {
    bind r = file_read_hex(sx_dir() + "/relics/pay/" + id)
    match r {
        bad(e) => { return "" }
        ok(v) => { return r.unwrap() }
    }
}

fn sx_pay_text(id) { return ut_read_text(sx_dir() + "/relics/pay/" + id) }

# ── spirit manifests ──────────────────────────────────────────────────
fn sx_spirit_exists(id) { return file_exists(sx_dir() + "/relics/spirit/" + id) }

# returns [strategy, a, b, size, kind, depth]
#   whole:  a=pay              delta: a=base spirit, b=pay (script)
#   chunks: a=pay (chunk list), b=count
fn sx_spirit_info(id) {
    bind t = ut_read_text(sx_dir() + "/relics/spirit/" + id)
    if t == "" { return ["", "", "", 0.0, "", 0.0] }
    bind lines = split(t, "\n")
    bind head = split(list_get(lines, 0.0), "|")
    bind strat = list_get(head, 0.0)
    bind a = list_get(head, 1.0)
    bind b = list_get(head, 2.0)
    bind size = 0.0
    bind kind = "bin"
    bind depth = 0.0
    bind i = 1.0
    while i < len(lines) {
        bind ln = list_get(lines, i)
        if str_starts_with(ln, "size|") { bind size = to_number(str_slice(ln, 5.0, 99.0)) }
        if str_starts_with(ln, "kind|") { bind kind = str_slice(ln, 5.0, 99.0) }
        if str_starts_with(ln, "depth|") { bind depth = to_number(str_slice(ln, 6.0, 99.0)) }
        bind i = i + 1.0
    }
    return [strat, a, b, size, kind, depth]
}

# ── materialize a spirit back into bytes/text ─────────────────────────
fn sx_mat_text(id) {
    bind info = sx_spirit_info(id)
    bind strat = list_get(info, 0.0)
    if strat == "delta" {
        bind base = sx_mat_text(list_get(info, 1.0))
        bind script = sx_pay_text(list_get(info, 2.0))
        return dy_delta_apply(base, script)
    }
    bind hx = sx_mat_hex(id)
    bind r = hex_text(hx)
    match r {
        bad(e) => { return "" }
        ok(v) => { return r.unwrap() }
    }
}

fn sx_mat_hex(id) {
    bind info = sx_spirit_info(id)
    bind strat = list_get(info, 0.0)
    if strat == "whole" { return sx_pay_hex(list_get(info, 1.0)) }
    if strat == "chunks" {
        bind lst = split(str_trim(sx_pay_text(list_get(info, 1.0))), "\n")
        bind parts = list_new()
        bind i = 0.0
        while i < len(lst) {
            bind cid = list_get(lst, i)
            if cid != "" { bind parts = list_push(parts, sx_pay_hex(cid)) }
            bind i = i + 1.0
        }
        return hex_concat(parts)
    }
    if strat == "delta" {
        return text_hex(sx_mat_text(id))
    }
    return ""
}

# write a spirit's bytes to a worktree path
fn sx_checkout_spirit(id, dest) {
    bind parent = ut_parent(dest)
    if parent != dest {
        if file_exists(parent) == false { make_dir(parent) }
    }
    bind hx = sx_mat_hex(id)
    file_write_hex(dest, hx)
    return 0.0
}

# ── THE DIVINER: adaptive enshrinement ────────────────────────────────
# Stores the file at abs_path as a spirit; prev_id is the previous spirit
# recorded for the same path ("" if none). Returns [spirit_id, strategy].
fn sx_spirit_put(abs_path, prev_id) {
    bind id = file_hash(abs_path)
    if id == "" { return ["", "unreadable"] }
    if sx_spirit_exists(id) { return [id, "kept"] }
    bind hr = file_read_hex(abs_path)
    match hr {
        bad(e) => { return ["", "unreadable"] }
        ok(v) => { bind hr = hr }
    }
    bind hx = hr.unwrap()
    bind size = len(hx) / 2.0
    # text divination
    bind is_text = false
    bind text = ""
    if size <= 4194304.0 {
        bind tr = hex_text(hx)
        if ut_is_bad(tr) == false {
            bind is_text = true
            bind text = tr.unwrap()
        }
    }
    # 1) try a delta against the previous incarnation of this path
    if is_text {
        if prev_id != "" {
            if sx_spirit_exists(prev_id) {
                bind pinfo = sx_spirit_info(prev_id)
                bind pkind = list_get(pinfo, 4.0)
                bind pdepth = list_get(pinfo, 5.0)
                if pkind == "text" {
                    if pdepth < 8.0 {
                        bind ptext = sx_mat_text(prev_id)
                        bind script = dy_delta_make(ptext, text)
                        if script != "" {
                            if len(script) < len(text) * 0.45 + 24.0 {
                                bind pay = sx_pay_put_text(script)
                                bind m = "delta|" + prev_id + "|" + pay + "\n"
                                bind m = m + "size|" + size + "\nkind|text\ndepth|" + (pdepth + 1.0) + "\n"
                                write_file(sx_dir() + "/relics/spirit/" + id, m)
                                return [id, "delta"]
                            }
                        }
                    }
                }
            }
        }
    }
    # 2) giant spirits get a distributed body (content-defined chunks)
    if size > 262144.0 {
        bind chunks = dy_chunks(hx)
        bind ids = ""
        bind i = 0.0
        while i < len(chunks) {
            bind cid = sx_pay_put_hex(list_get(chunks, i))
            bind ids = ids + cid + "\n"
            bind i = i + 1.0
        }
        bind lpay = sx_pay_put_text(ids)
        bind kind = ut_sel(is_text, "text", "bin")
        bind m = "chunks|" + lpay + "|" + len(chunks) + "\n"
        bind m = m + "size|" + size + "\nkind|" + kind + "\n"
        write_file(sx_dir() + "/relics/spirit/" + id, m)
        return [id, "chunks " + len(chunks)]
    }
    # 3) small spirits are enshrined whole
    bind pay = sx_pay_put_hex(hx)
    bind kind = ut_sel(is_text, "text", "bin")
    bind m = "whole|" + pay + "\n" + "size|" + size + "\nkind|" + kind + "\n"
    write_file(sx_dir() + "/relics/spirit/" + id, m)
    return [id, "whole"]
}

# ── scrolls (tree manifests) and rites (commits) ──────────────────────
# a scroll payload: sorted lines "<spirit-id> <path>"
fn sx_scroll_put(ids, paths) {
    bind sp = ut_sort_pair(paths, ids)
    bind ps = list_get(sp, 0.0)
    bind is = list_get(sp, 1.0)
    bind t = ""
    bind i = 0.0
    while i < len(ps) {
        bind t = t + list_get(is, i) + " " + list_get(ps, i) + "\n"
        bind i = i + 1.0
    }
    return sx_pay_put_text(t)
}

# returns [ids, paths]
fn sx_scroll_read(pay) {
    bind t = sx_pay_text(pay)
    bind ids = list_new()
    bind paths = list_new()
    if t == "" { return [ids, paths] }
    bind lines = split(t, "\n")
    bind i = 0.0
    while i < len(lines) {
        bind ln = list_get(lines, i)
        if ln != "" {
            bind sp = str_find(ln, " ")
            if sp > 0.0 {
                bind ids = list_push(ids, str_slice(ln, 0.0, sp))
                bind paths = list_push(paths, str_slice(ln, sp + 1.0, 99999.0))
            }
        }
        bind i = i + 1.0
    }
    return [ids, paths]
}

fn sx_rite_put(scrollpay, parents, author, msg) {
    bind t = "scroll " + scrollpay + "\n"
    bind i = 0.0
    while i < len(parents) {
        bind p = list_get(parents, i)
        if p != "" { bind t = t + "parent " + p + "\n" }
        bind i = i + 1.0
    }
    bind t = t + "author " + author + "\n"
    bind t = t + "time " + floor(now_unix()) + "\n"
    bind t = t + "\n" + msg + "\n"
    bind id = hash_hex(t)
    write_file(sx_dir() + "/relics/rite/" + id, t)
    return id
}

# returns [scrollpay, parents(list), author, time, message]
fn sx_rite_read(id) {
    bind t = ut_read_text(sx_dir() + "/relics/rite/" + id)
    bind scroll = ""
    bind parents = list_new()
    bind author = ""
    bind time = 0.0
    bind msg = ""
    if t == "" { return [scroll, parents, author, time, msg] }
    bind lines = split(t, "\n")
    bind i = 0.0
    bind in_msg = false
    while i < len(lines) {
        bind ln = list_get(lines, i)
        if in_msg {
            bind msg = msg + ln + "\n"
        } else if ln == "" {
            bind in_msg = true
        } else if str_starts_with(ln, "scroll ") {
            bind scroll = str_slice(ln, 7.0, 99999.0)
        } else if str_starts_with(ln, "parent ") {
            bind parents = list_push(parents, str_slice(ln, 7.0, 99999.0))
        } else if str_starts_with(ln, "author ") {
            bind author = str_slice(ln, 7.0, 99999.0)
        } else if str_starts_with(ln, "time ") {
            bind time = to_number(str_slice(ln, 5.0, 99.0))
        }
        bind i = i + 1.0
    }
    bind msg = str_trim(msg)
    return [scroll, parents, author, time, msg]
}

# manifest of a rite -> [ids, paths]; "" rite -> empty
fn sx_rite_manifest(rite) {
    if rite == "" { return [list_new(), list_new()] }
    bind r = sx_rite_read(rite)
    return sx_scroll_read(list_get(r, 0.0))
}

# ── the altar (staging overlay) ───────────────────────────────────────
# altar lines: "<spirit-id>|<path>", spirit-id "-" marks a staged removal
fn sx_altar() {
    bind lines = ut_read_lines(sx_dir() + "/altar")
    bind ids = list_new()
    bind paths = list_new()
    bind i = 0.0
    while i < len(lines) {
        bind ln = list_get(lines, i)
        bind sp = str_find(ln, "|")
        if sp > 0.0 {
            bind ids = list_push(ids, str_slice(ln, 0.0, sp))
            bind paths = list_push(paths, str_slice(ln, sp + 1.0, 99999.0))
        }
        bind i = i + 1.0
    }
    return [ids, paths]
}

fn sx_altar_write(ids, paths) {
    bind t = ""
    bind i = 0.0
    while i < len(ids) {
        bind t = t + list_get(ids, i) + "|" + list_get(paths, i) + "\n"
        bind i = i + 1.0
    }
    write_file(sx_dir() + "/altar", t)
    return 0.0
}

fn sx_altar_set(path, id) {
    bind a = sx_altar()
    bind ids = list_get(a, 0.0)
    bind paths = list_get(a, 1.0)
    bind at = ut_index_of(paths, path)
    if at >= 0.0 {
        bind ids = list_set(ids, at, id)
    } else {
        bind ids = list_push(ids, id)
        bind paths = list_push(paths, path)
    }
    sx_altar_write(ids, paths)
    return 0.0
}

fn sx_altar_del(path) {
    bind a = sx_altar()
    bind ids = list_get(a, 0.0)
    bind paths = list_get(a, 1.0)
    bind nids = list_new()
    bind npaths = list_new()
    bind i = 0.0
    while i < len(ids) {
        if list_get(paths, i) != path {
            bind nids = list_push(nids, list_get(ids, i))
            bind npaths = list_push(npaths, list_get(paths, i))
        }
        bind i = i + 1.0
    }
    sx_altar_write(nids, npaths)
    return 0.0
}

# head manifest + altar overlay = the next rite's manifest. returns [ids, paths]
fn sx_overlay() {
    bind head = sx_rite_manifest(sx_head_rite())
    bind hids = list_get(head, 0.0)
    bind hpaths = list_get(head, 1.0)
    bind a = sx_altar()
    bind aids = list_get(a, 0.0)
    bind apaths = list_get(a, 1.0)
    bind ids = list_new()
    bind paths = list_new()
    bind i = 0.0
    while i < len(hpaths) {
        bind p = list_get(hpaths, i)
        bind at = ut_index_of(apaths, p)
        if at < 0.0 {
            bind ids = list_push(ids, list_get(hids, i))
            bind paths = list_push(paths, p)
        } else if list_get(aids, at) != "-" {
            bind ids = list_push(ids, list_get(aids, at))
            bind paths = list_push(paths, p)
        }
        bind i = i + 1.0
    }
    # altar entries for brand new paths
    bind i = 0.0
    while i < len(apaths) {
        bind p = list_get(apaths, i)
        if ut_index_of(hpaths, p) < 0.0 {
            if list_get(aids, i) != "-" {
                bind ids = list_push(ids, list_get(aids, i))
                bind paths = list_push(paths, p)
            }
        }
        bind i = i + 1.0
    }
    return [ids, paths]
}

# ── ignore rules ──────────────────────────────────────────────────────
fn sx_ign_rules() {
    if st_has("sx_ign") > 0.5 { return st_get("sx_ign", []) }
    # cache/ and screenshots/ are droppings of the Ling runtime itself
    # (font raster cache, the screenshot() builtin) — never spirits.
    bind rules = [".miao", ".git", ".ling-ssh", ".ling-build", "cache", "screenshots"]
    bind extra = ut_read_lines(sx_root() + "/.miaoignore")
    bind i = 0.0
    while i < len(extra) {
        bind ln = str_trim(list_get(extra, i))
        if ln != "" {
            if str_starts_with(ln, "#") == false { bind rules = list_push(rules, ln) }
        }
        bind i = i + 1.0
    }
    bind extra2 = ut_read_lines(sx_root() + "/ling.ignore")
    bind i = 0.0
    while i < len(extra2) {
        bind ln = str_trim(list_get(extra2, i))
        if ln != "" {
            if str_starts_with(ln, "#") == false { bind rules = list_push(rules, ln) }
        }
        bind i = i + 1.0
    }
    st_set("sx_ign", rules)
    return rules
}

fn sx_ignored(rel) {
    bind rules = sx_ign_rules()
    bind i = 0.0
    while i < len(rules) {
        bind pat = list_get(rules, i)
        if pat == rel { return true }
        if str_ends_with(pat, "/") {
            bind d = str_slice(pat, 0.0, len(pat) - 1.0)
            if rel == d { return true }
            if str_starts_with(rel, pat) { return true }
        }
        if str_starts_with(pat, "*.") {
            if str_ends_with(rel, str_slice(pat, 1.0, 99999.0)) { return true }
        }
        if str_ends_with(pat, "/*") {
            if str_starts_with(rel, str_slice(pat, 0.0, len(pat) - 1.0)) { return true }
        }
        if str_find(pat, "/") < 0.0 {
            if str_find(pat, "*") < 0.0 {
                # bare name: match as path segment anywhere
                if str_starts_with(rel, pat + "/") { return true }
                if str_ends_with(rel, "/" + pat) { return true }
                if str_find(rel, "/" + pat + "/") >= 0.0 { return true }
            }
        }
        bind i = i + 1.0
    }
    return false
}

# ── worktree walk ─────────────────────────────────────────────────────
fn sx_walk_dir(abs, rel, acc) {
    bind es = list_dir(abs)
    bind i = 0.0
    while i < len(es) {
        bind e = list_get(es, i)
        bind name = path_name(e)
        bind rel2 = name
        if rel != "" { bind rel2 = rel + "/" + name }
        if sx_ignored(rel2) == false {
            if is_dir(e) {
                bind acc = sx_walk_dir(e, rel2, acc)
            } else {
                bind acc = list_push(acc, rel2)
            }
        }
        bind i = i + 1.0
    }
    return acc
}

fn sx_walk() {
    return ut_sort(sx_walk_dir(sx_root(), "", list_new()))
}

# ── chronicle (reflog) ────────────────────────────────────────────────
fn sx_log(op, detail) {
    bind p = sx_dir() + "/chronicle"
    bind old = ut_read_text(p)
    write_file(p, old + floor(now_unix()) + "|" + op + "|" + detail + "\n")
    return 0.0
}
