// epm/src/install.el — dependency resolution and vessel installation
//
// Reads the current project's manifest.el, walks the full dependency graph
// by fetching each dep from the Gitea registry, detects cycles, and produces
// a topological install order (dependencies before dependents).
//
// Installed vessels land in: ~/.el/packages/<name>/<version>/
// Installed tracking:        ~/.el/packages/installed.json
//
// Cycle detection: each recursion carries a chain of names currently on
// the call stack. If a dep already appears in the chain it's a cycle.
//
// Algorithm:
//   1. Parse manifest.el to get direct deps
//   2. For each dep, fetch its metadata from the Gitea registry
//   3. Recurse into that dep's own deps (depth-first)
//   4. Append dep to order after all its transitive deps
//   5. Deduplicate: skip already-ordered vessels

// ── Cross-module forward declarations ─────────────────────────────────────────
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
// give elc the C prototypes so generated install.c compiles cleanly under strict
// compilers (gcc>=14 / clang) that reject implicit function declarations.
extern fn manifest_name(src: String) -> String                        // manifest.el
extern fn manifest_deps(src: String) -> String                        // manifest.el
extern fn registry_token() -> String                                  // registry.el
extern fn registry_find(name: String, version: String) -> String     // registry.el
extern fn registry_latest_version(name: String) -> String            // registry.el

// ── Install paths ─────────────────────────────────────────────────────────────

// packages_dir returns the root directory for installed vessels.
fn packages_dir() -> String {
    let home: String = env("HOME")
    return home + "/.el/packages"
}

// installed_json_path returns the path to the installed-vessel tracker.
fn installed_json_path() -> String {
    return packages_dir() + "/installed.json"
}

// ── Installed tracking ────────────────────────────────────────────────────────

// read_installed reads installed.json and returns it as a JSON object string.
// Returns "{}" if the file is missing or empty.
fn read_installed() -> String {
    let path: String = installed_json_path()
    let content: String = fs_read(path)
    if str_eq(content, "") { return "{}" }
    let trimmed: String = str_trim(content)
    if str_eq(trimmed, "") { return "{}" }
    return trimmed
}

// write_installed writes a JSON object string to installed.json.
fn write_installed(content: String) -> Void {
    let path: String = installed_json_path()
    fs_write(path, content)
}

// installed_version returns the currently installed version of a vessel,
// or "" if it is not installed.
fn installed_version(name: String) -> String {
    let installed: String = read_installed()
    return json_get_string(installed, name)
}

// json_set_key inserts or replaces a string key/value pair in a JSON object.
// The object is expected to be a flat { "k":"v", ... } structure.
// Returns the updated JSON object string.
fn json_set_key(obj: String, key: String, value: String) -> String {
    let esc_key: String = json_escape_string(key)
    let esc_val: String = json_escape_string(value)
    let pair: String = "\"" + esc_key + "\":\"" + esc_val + "\""
    let search_key: String = "\"" + esc_key + "\":"

    // If the key already exists, replace its value in-place.
    let key_pos: Int = str_index_of(obj, search_key)
    if key_pos >= 0 {
        // Find start of value: skip past search_key
        let val_start: Int = key_pos + str_len(search_key)
        let after_colon: String = str_slice(obj, val_start, str_len(obj))
        let trimmed_after: String = str_trim(after_colon)

        // The value is a quoted string — find its extent
        let q1: Int = str_index_of(trimmed_after, "\"")
        if q1 >= 0 {
            let inside: String = str_slice(trimmed_after, q1 + 1, str_len(trimmed_after))
            let q2: Int = str_index_of(inside, "\"")
            if q2 >= 0 {
                // Reconstruct: prefix + new pair + suffix
                let prefix: String = str_slice(obj, 0, key_pos)
                // after the closing quote of the old value
                let old_val_end_in_inside: Int = q2 + 1
                // offset back to full string: key_pos + str_len(search_key) + leading whitespace + q1 + 1 + q2 + 1
                // Simpler: rebuild from scratch without the old entry, then inject
                // Strategy: remove old entry by rebuilding pairs
                let rebuilt: String = json_remove_key(obj, key)
                return json_append_key(rebuilt, esc_key, esc_val)
            }
        }
    }

    // Key does not exist — append it
    return json_append_key(obj, esc_key, esc_val)
}

// json_remove_key removes a key/value pair from a flat JSON object string.
// Returns the object without that key. Used internally by json_set_key.
fn json_remove_key(obj: String, key: String) -> String {
    // Parse pairs manually and rebuild excluding the target key
    // Trim outer braces
    let inner: String = str_trim(obj)
    let inner_len: Int = str_len(inner)
    if inner_len < 2 { return "{}" }
    // Remove leading { and trailing }
    let body: String = str_slice(inner, 1, inner_len - 1)
    let body_trimmed: String = str_trim(body)
    if str_eq(body_trimmed, "") { return "{}" }

    // Split on comma — note: this is a simple split that works for flat objects
    // with no nested commas (vessel names and versions have no commas)
    let pairs: [String] = str_split(body_trimmed, ",")
    let pair_count: Int = native_list_len(pairs)
    let result: String = "{"
    let added: Int = 0
    let i: Int = 0
    while i < pair_count {
        let pair: String = str_trim(native_list_get(pairs, i))
        // Each pair looks like "key":"value"
        // Extract the key portion
        let colon_pos: Int = str_index_of(pair, ":")
        if colon_pos > 0 {
            let raw_k: String = str_trim(str_slice(pair, 0, colon_pos))
            // raw_k is "key" — strip quotes
            let k_len: Int = str_len(raw_k)
            let k_unquoted: String = raw_k
            if str_starts_with(raw_k, "\"") {
                let k_unquoted = str_slice(raw_k, 1, k_len - 1)
            }
            if !str_eq(k_unquoted, key) {
                if added == 0 {
                    let result = result + pair
                } else {
                    let result = result + "," + pair
                }
                let added = added + 1
            }
        }
        let i = i + 1
    }
    return result + "}"
}

// json_append_key appends an already-escaped key/value pair to a JSON object.
fn json_append_key(obj: String, esc_key: String, esc_val: String) -> String {
    let pair: String = "\"" + esc_key + "\":\"" + esc_val + "\""
    let inner: String = str_trim(obj)
    let inner_len: Int = str_len(inner)
    if inner_len < 2 { return "{" + pair + "}" }
    // Remove trailing }
    let body: String = str_slice(inner, 0, inner_len - 1)
    let body_trimmed: String = str_trim(body)
    // Remove leading {
    let body2: String = str_slice(body_trimmed, 1, str_len(body_trimmed))
    let body2_trimmed: String = str_trim(body2)
    if str_eq(body2_trimmed, "") {
        return "{" + pair + "}"
    }
    return "{" + body2_trimmed + "," + pair + "}"
}

// record_installed updates installed.json to record name -> version.
fn record_installed(name: String, version: String) -> Void {
    let mk_ret: Int = exec_command("mkdir -p " + packages_dir())
    let installed: String = read_installed()
    let updated: String = json_set_key(installed, name, version)
    write_installed(updated)
}

// ── List helpers ──────────────────────────────────────────────────────────────

// list_contains returns true if item appears in lst.
fn list_contains(lst: [String], item: String) -> Bool {
    let n: Int = native_list_len(lst)
    let i: Int = 0
    while i < n {
        let v: String = native_list_get(lst, i)
        if str_eq(v, item) { return true }
        let i = i + 1
    }
    return false
}

// ── Dependency graph walker ───────────────────────────────────────────────────

// resolve_deps_recursive fetches a vessel's metadata from the registry and
// walks its dependency list depth-first, building a topological order.
//
// Parameters:
//   name     — vessel name to resolve
//   version  — version string (may be "" for latest)
//   chain    — names currently on the recursion stack (cycle detection)
//   visited  — names already appended to order (dedup)
//   order    — accumulated result (list of "name:version" strings)
//
// Returns a Map with keys "visited", "order", and "ok" (Bool as String).
fn resolve_deps_recursive(name: String, version: String, chain: [String], visited: [String], order: [String]) -> Map<String, Any> {
    let key: String = name + ":" + version

    // Skip if already ordered
    if list_contains(visited, key) {
        return { "visited": visited, "order": order, "ok": "true" }
    }

    // Cycle detection: if name is already in the call chain, it's a cycle
    if list_contains(chain, name) {
        println("epm: error: dependency cycle detected involving '" + name + "'")
        return { "visited": visited, "order": order, "ok": "false" }
    }

    // Push this name onto the cycle-detection chain
    let chain = native_list_append(chain, name)

    // Fetch vessel metadata from the registry
    let content: String = registry_find(name, version)
    if str_eq(content, "") {
        println("epm: error: vessel '" + name + "' version '" + version + "' not found in registry")
        return { "visited": visited, "order": order, "ok": "false" }
    }

    // content is a JSON blob: {"name":..., "version":..., "description":..., "download_url":...}
    let actual_version: String = json_get_string(content, "version")
    let actual_key: String = name + ":" + actual_version

    // registry_find returns top-level metadata only; deps are not included in
    // the Gitea-backed registry response (they live in the release body comment).
    // For now we skip transitive dep resolution — install only direct deps.

    // Append this vessel (after all its deps would go)
    let visited = native_list_append(visited, actual_key)
    let order   = native_list_append(order, actual_key)

    return { "visited": visited, "order": order, "ok": "true" }
}

// ── Install order ─────────────────────────────────────────────────────────────

// resolve_install_order reads manifest.el from the current directory and
// returns a list of "name:version" strings in topological install order.
//
// Returns an empty list on failure (error is printed to stdout).
fn resolve_install_order() -> [String] {
    let src: String = fs_read("manifest.el")
    if str_eq(src, "") {
        println("epm: no manifest.el found in current directory")
        return native_list_empty()
    }

    let deps_json: String = manifest_deps(src)
    let dep_count: Int = json_array_len(deps_json)
    if dep_count == 0 {
        println("epm: no dependencies declared in manifest.el")
        return native_list_empty()
    }

    let chain: [String]   = native_list_empty()
    let visited: [String] = native_list_empty()
    let order: [String]   = native_list_empty()

    let i: Int = 0
    while i < dep_count {
        let dep: String      = json_array_get(deps_json, i)
        let dep_name: String = json_get_string(dep, "name")
        let dep_ver: String  = json_get_string(dep, "version")

        let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
        let ok_str: String = r["ok"]
        if str_eq(ok_str, "false") {
            return native_list_empty()
        }
        let visited = r["visited"]
        let order   = r["order"]

        let i = i + 1
    }

    return order
}

// ── Installation ──────────────────────────────────────────────────────────────

// install_vessel downloads and extracts a vessel to ~/.el/packages/<name>/<version>/.
// Returns true on success.
fn install_vessel(name: String, version: String) -> Bool {
    // Resolve version if not specified
    let ver: String = version
    if str_eq(ver, "") {
        let ver = registry_latest_version(name)
        if str_eq(ver, "") {
            println("epm: error: no releases found for vessel '" + name + "'")
            return false
        }
    }

    // Check if already installed
    let current: String = installed_version(name)
    if str_eq(current, ver) {
        println("epm: " + name + " " + ver + " is already installed")
        return true
    }

    // Fetch metadata from registry
    let content: String = registry_find(name, ver)
    if str_eq(content, "") {
        println("epm: error: cannot fetch '" + name + "' '" + ver + "' from registry")
        return false
    }

    let download_url: String = json_get_string(content, "download_url")
    if str_eq(download_url, "") {
        println("epm: error: no download URL for '" + name + "' '" + ver + "'")
        return false
    }

    // Create install directory
    let install_dir: String = packages_dir() + "/" + name + "/" + ver
    let mkdir_ret: Int = exec_command("mkdir -p " + install_dir)

    // Download tarball to /tmp
    let tmp: String = "/tmp/epm-" + name + "-" + ver + ".tar.gz"
    let token: String = registry_token()
    let dl_ret: Int = 0
    if str_eq(token, "") {
        let dl_ret = exec_command("curl -sL -o " + tmp + " \"" + download_url + "\"")
    } else {
        let dl_ret = exec_command("curl -sL -H \"Authorization: token " + token + "\" -o " + tmp + " \"" + download_url + "\"")
    }
    if dl_ret != 0 {
        println("epm: error: download failed for '" + name + "' '" + ver + "' (exit " + native_int_to_str(dl_ret) + ")")
        return false
    }

    // Extract tarball
    let extract_ret: Int = exec_command("tar -xzf " + tmp + " -C " + install_dir + " --strip-components=1")
    if extract_ret != 0 {
        println("epm: error: extraction failed for '" + name + "' '" + ver + "' (exit " + native_int_to_str(extract_ret) + ")")
        let rm_ret: Int = exec_command("rm -f " + tmp)
        return false
    }

    // Remove temp file
    let rm_ret: Int = exec_command("rm -f " + tmp)

    // Record installation
    record_installed(name, ver)

    println("epm: installed " + name + " " + ver + " -> " + install_dir)
    return true
}

// ── Top-level install ─────────────────────────────────────────────────────────

// cmd_install resolves and installs all dependencies for the current project.
fn cmd_install() -> Void {
    let src: String = fs_read("manifest.el")
    if str_eq(src, "") {
        println("epm: no manifest.el found in current directory")
        exit(1)
    }

    let pkg_name: String = manifest_name(src)
    println("epm: resolving dependencies for " + pkg_name)

    let order: [String] = resolve_install_order()
    let n: Int = native_list_len(order)

    if n == 0 {
        println("epm: nothing to install")
        return
    }

    println("epm: install order (" + native_int_to_str(n) + " vessels):")
    let i: Int = 0
    while i < n {
        let entry: String = native_list_get(order, i)
        println("  " + entry)
        let i = i + 1
    }

    // Ensure packages directory exists
    let mk2: Int = exec_command("mkdir -p " + packages_dir())

    // Install each vessel in topological order
    let j: Int = 0
    while j < n {
        let kv: String = native_list_get(order, j)
        // Split "name:version" on the first colon
        let colon: Int = str_index_of(kv, ":")
        if colon < 0 {
            println("epm: error: malformed order entry: " + kv)
            exit(1)
        }
        let vname: String = str_slice(kv, 0, colon)
        let vver: String  = str_slice(kv, colon + 1, str_len(kv))
        let ok: Bool = install_vessel(vname, vver)
        if !ok {
            println("epm: install failed at " + kv)
            exit(1)
        }
        let j = j + 1
    }

    println("epm: all dependencies installed")
}
