# Cache Core — reads from the shared plugin cache directory.
# Written by ncl-sync daemon; read by this module and nu_plugin_nickel.
# Single writer principle: Nu NEVER writes to the cache dir directly.

use ./metadata.nu *

# Check if a directory has workspace markers.
def is-ws-dir [path: string]: nothing -> bool {
    if ($path | is-empty) or (not ($path | path exists)) { return false }
    let has_infra  = ($path | path join "infra" | path exists)
    let has_config = ($path | path join "config" "provisioning.ncl" | path exists)
    let has_onto   = ($path | path join ".ontology" | path exists)
    $has_infra or $has_config or $has_onto
}

# Walk up from PWD to find workspace root (recursive).
def find-ws-up [path: string]: nothing -> string {
    if ($path | is-empty) or $path == "/" { return "" }
    if (is-ws-dir $path) { return $path }
    let parent = ($path | path dirname)
    if $parent == $path { return "" }
    find-ws-up $parent
}

# Global cache directory (shared across workspaces, for files under $PROVISIONING).
def get-global-cache-dir []: nothing -> string {
    let home = ($env.HOME? | default "~" | path expand)
    let host_info = (do { sys host } | complete)
    let is_mac = if $host_info.exit_code == 0 {
        ($host_info.stdout | get name | str downcase | str contains "darwin")
            or ($host_info.stdout | get name | str downcase | str contains "macos")
    } else {
        ($home | path join "Library" | path exists)
    }
    if $is_mac {
        $home | path join "Library" "Caches" "provisioning" "config-cache"
    } else {
        $home | path join ".cache" "provisioning" "config-cache"
    }
}

# Resolve cache directory FOR A SPECIFIC FILE. Priority:
#   1. $NCL_CACHE_DIR (explicit override, for CI/tests)
#   2. File under $PROVISIONING → global cache (extensions, schemas — shared)
#   3. File under a workspace (walk up from file path) → <ws>/.ncl-cache/
#   4. Fallback: global cache
#
# Must match resolve_cache_dir_for_file() in ncl-sync + plugin.
def get-cache-dir-for-file [file_path: string]: nothing -> string {
    if ($env.NCL_CACHE_DIR? | is-not-empty) {
        return $env.NCL_CACHE_DIR
    }
    # File under $PROVISIONING → global cache
    let prov = ($env.PROVISIONING? | default "")
    if ($prov | is-not-empty) and ($file_path | str starts-with $prov) {
        return (get-global-cache-dir)
    }
    # File under a workspace → workspace-local cache
    let ws = (find-ws-up ($file_path | path dirname))
    if ($ws | is-not-empty) {
        return ($ws | path join ".ncl-cache")
    }
    get-global-cache-dir
}

# Legacy helper (CWD-based) — kept for backwards compat in code paths that don't have
# the file path at hand. Prefer get-cache-dir-for-file.
def get-cache-base-dir []: nothing -> string {
    if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR }
    let ws = (find-ws-up $env.PWD)
    if ($ws | is-not-empty) { return ($ws | path join ".ncl-cache") }
    get-global-cache-dir
}

# Lookup a cache entry by pre-computed key.
# Only "nickel" type is backed by the shared plugin cache.
# Returns: { valid: bool, reason: string, data: any }
export def cache-lookup [
    cache_type: string
    cache_key: string
    --ttl: int = 0
]: nothing -> record {
    if $cache_type != "nickel" {
        return { valid: false, reason: "type_not_supported", data: null }
    }
    let cache_file = ((get-cache-base-dir) | path join $"($cache_key).json")
    if not ($cache_file | path exists) {
        return { valid: false, reason: "cache_miss", data: null }
    }
    let result = (do { open $cache_file } | complete)
    if $result.exit_code != 0 {
        return { valid: false, reason: "read_error", data: null }
    }
    { valid: true, reason: "hit", data: ($result.stdout | from json) }
}

# Signal ncl-sync daemon to (re-)export a list of NCL files.
# Nu never writes to the cache directly — only signals the daemon.
# Uses pid-unique sidecar + atomic rename to prevent concurrent-write corruption.
export def cache-write [
    cache_type: string
    cache_key: string
    data: any
    source_files: list
    --ttl: int = 0
]: nothing -> nothing {
    if $cache_type != "nickel" { return }
    write-sync-request ($source_files | each {|f| { path: $f, import_paths: [] }})
}

# Write a sync-request sidecar file for ncl-sync to process.
# Each Nu process writes .sync-<pid>.tmp then renames to .sync-<pid>.json atomically.
export def write-sync-request [
    requests: list  # list of {path: string, import_paths: list}
]: nothing -> nothing {
    let cache_dir = (get-cache-base-dir)
    if not ($cache_dir | path exists) { return }
    let pid = $nu.pid
    let tmp_file  = ($cache_dir | path join $".sync-($pid).tmp")
    let json_file = ($cache_dir | path join $".sync-($pid).json")
    $requests | to json | save --force $tmp_file
    ^mv $tmp_file $json_file
}

# Cache stats — count entries and total size in the shared cache dir.
export def get-cache-stats []: nothing -> record {
    let cache_dir = (get-cache-base-dir)
    if not ($cache_dir | path exists) {
        return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
    }
    let files = (do { ls $cache_dir } | complete)
    if $files.exit_code != 0 {
        return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
    }
    let entries = ($files.stdout | where name =~ '\.json$' | where name !~ 'manifest' | length)
    let size_bytes = ($files.stdout | where name =~ '\.json$' | get size | math sum)
    {
        total_entries: $entries,
        total_size_mb: ($size_bytes / 1_048_576 | math round -p 2),
        by_type: { nickel: $entries }
    }
}

# Clear the shared cache directory (removes all .json files except manifest).
export def cache-clear-type [cache_type: string]: nothing -> nothing {
    if $cache_type != "nickel" { return }
    let cache_dir = (get-cache-base-dir)
    if not ($cache_dir | path exists) { return }
    do {
        ls $cache_dir
        | where name =~ '\.json$'
        | where name !~ 'manifest'
        | each {|f| rm $f.name}
    } | ignore
}

# No-op — eviction is handled by ncl-sync daemon.
export def cleanup-expired-cache [max_size_mb: int = 100]: nothing -> nothing {}
