use ../lib_provisioning/workspace *
use ../lib_provisioning/user/config.nu [get-workspace-path, get-active-workspace-details]
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval]

# Decrypt a SOPS-encrypted dotenv file and return its contents as a record.
#
# The file must be in dotenv format (KEY=VALUE lines). SOPS is called with
# --output-type=dotenv so the decrypted output is in the same format.
# Lines starting with # and blank lines are ignored.
#
# Auto-discovery: if secrets_path is empty, looks for cluster/<cluster>/secrets.sops.env
# relative to ws_root. Returns {} if no secrets file is found and path was not explicit.
def cd-load-secrets [secrets_path: string]: nothing -> record {
    if (($secrets_path | path exists) == false) {
        error make { msg: $"Secrets file not found: ($secrets_path)" }
    }
    let result = (do { ^sops --decrypt --output-type=dotenv $secrets_path } | complete)
    if $result.exit_code != 0 {
        error make { msg: $"SOPS decrypt failed for ($secrets_path):\n($result.stderr)" }
    }
    $result.stdout
    | lines
    | where { ($in | str starts-with "#") == false }
    | where { ($in | str contains "=") }
    | parse "{key}={value}"
    | reduce --fold {} {|row, acc| $acc | insert $row.key $row.value }
}

# Export a Nickel file relative to the workspace root, with workspace and provisioning import paths.
def cd-ncl-export [ws_root: string, rel_path: string]: nothing -> record {
    let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
    let full_path = ($ws_root | path join $rel_path)
    let result = (ncl-eval $full_path [$ws_root $prov_root])
    $result
}

# Read .provisioning-state.json and return FIP env vars (FIP_A_IP/ID, FIP_B_IP/ID, FIP_C_IP/ID).
#
# FIP key mapping (set by bootstrap.nu naming convention after stripping "librecloud-fip-" prefix
# and replacing dashes with underscores):
#   smtp           → FIP_A  (Stalwart SMTP, sgoyol-1)
#   sgoyol_ingress → FIP_B  (sgoyol Cilium ingress)
#   wuji           → FIP_C  (wuji K8s API + ingress)
def cd-load-fip-env [ws_root: string]: nothing -> record {
    let state_path = ($ws_root | path join ".provisioning-state.json")
    if (($state_path | path exists) == false) {
        error make { msg: ".provisioning-state.json not found — run: provisioning bootstrap first" }
    }
    let state = (open --raw $state_path | from json)
    let fips  = ($state | get bootstrap | get floating_ips)
    let fip_a = ($fips | get -o smtp           | default {})
    let fip_b = ($fips | get -o sgoyol_ingress | default {})
    let fip_c = ($fips | get -o wuji           | default {})
    {
        FIP_A_IP: ($fip_a | get -o ip | default ""),
        FIP_A_ID: ($fip_a | get -o id | default ""),
        FIP_B_IP: ($fip_b | get -o ip | default ""),
        FIP_B_ID: ($fip_b | get -o id | default ""),
        FIP_C_IP: ($fip_c | get -o ip | default ""),
        FIP_C_ID: ($fip_c | get -o id | default ""),
    }
}

# Build env var record for an extension install script.
#
# Protocol: scalar fields → `<PREFIX>_<FIELD>`, lists/records → `<PREFIX>_<FIELD>_JSON`.
# Full config also available as `<PREFIX>_CONFIG_JSON`. FIP vars and KUBECONFIG always set.
def cd-ext-env [ext_name: string, cfg: any, fip_env: record, kubeconfig: string]: nothing -> record {
    let prefix = ($ext_name | str upcase | str replace --all "-" "_" | str replace --all "." "_")
    let flat = if ($cfg | describe | str starts-with "record") {
        $cfg | transpose key val | reduce --fold {} {|entry, acc|
            let raw_key   = ($entry.key | str upcase | str replace --all "-" "_" | str replace --all "." "_")
            let type_desc = ($entry.val | describe)
            let is_scalar = ($type_desc in ["string", "int", "float", "bool"])
            let env_key   = if $is_scalar { $"($prefix)_($raw_key)" } else { $"($prefix)_($raw_key)_JSON" }
            let env_val   = if $type_desc == "string" {
                $entry.val
            } else if $is_scalar {
                $entry.val | into string
            } else {
                $entry.val | to json --raw
            }
            $acc | insert $env_key $env_val
        }
    } else {
        {}
    }
    $flat
    | insert $"($prefix)_CONFIG_JSON" ($cfg | to json --raw)
    | merge $fip_env
    | insert KUBECONFIG $kubeconfig
}

# Locate the install script for an extension under extensions/clusters/.
#
# Extensions have inconsistent naming: some dirs use underscores (cert_manager, hcloud_floater)
# while scripts use dashes (install-cert-manager.sh, install-hcloud-floater.sh). Others are
# all-dash (oci-reg) or all-same (metallb, git, woodpecker, stalwart).
# Tries all 4 combinations of (dir: _ or -) × (script: _ or -).
def cd-find-script [prov_root: string, ext_name: string]: nothing -> string {
    let dash_name  = ($ext_name | str replace --all "_" "-")
    let under_name = ($ext_name | str replace --all "-" "_")
    # Pairs of [dir_name, script_name] — ordered by most-likely match first.
    let combos = [
        [$under_name, $under_name],
        [$under_name, $dash_name],
        [$dash_name,  $dash_name],
        [$dash_name,  $under_name],
    ]
    let found = ($combos | each {|pair|
        let p = ($prov_root | path join "extensions/clusters" $pair.0 "default" $"install-($pair.1).sh")
        if ($p | path exists) { $p } else { null }
    } | where { $in != null })
    if ($found | is-empty) {
        error make { msg: $"No install script for extension '($ext_name)' in ($prov_root)/extensions/clusters/ (tried all _/- variants)" }
    }
    $found | first
}

# Locate the install script for a component under extensions/components/.
#
# Components are structured as extensions/components/{comp_name}/{mode}/install-{comp_name}.sh.
# Tries all 4 combinations of dir/script name with dashes and underscores.
def cd-find-component-script [prov_root: string, comp_name: string, mode: string]: nothing -> string {
    let dash_name  = ($comp_name | str replace --all "_" "-")
    let under_name = ($comp_name | str replace --all "-" "_")
    let combos = [
        [$under_name, $under_name],
        [$under_name, $dash_name],
        [$dash_name,  $dash_name],
        [$dash_name,  $under_name],
    ]
    let found = ($combos | each {|pair|
        let p = ($prov_root | path join "extensions/components" $pair.0 $mode $"install-($pair.1).sh")
        if ($p | path exists) { $p } else { null }
    } | where { $in != null })
    if ($found | is-empty) {
        error make { msg: $"No install script for component '($comp_name)' mode '($mode)' in ($prov_root)/extensions/components/ (tried all _/- variants)" }
    }
    $found | first
}

# Non-erroring variant for dry-run display — returns "<not found>" if no component script exists.
def cd-find-component-script-opt [prov_root: string, comp_name: string, mode: string]: nothing -> string {
    let dash_name  = ($comp_name | str replace --all "_" "-")
    let under_name = ($comp_name | str replace --all "-" "_")
    let combos = [
        [$under_name, $under_name],
        [$under_name, $dash_name],
        [$dash_name,  $dash_name],
        [$dash_name,  $under_name],
    ]
    let found = ($combos | each {|pair|
        let p = ($prov_root | path join "extensions/components" $pair.0 $mode $"install-($pair.1).sh")
        if ($p | path exists) { $p } else { null }
    } | where { $in != null })
    if ($found | is-empty) { "<not found>" } else { $found | first }
}

# Non-erroring variant for dry-run display — returns "<not found>" if no script exists.
def cd-find-script-opt [prov_root: string, ext_name: string]: nothing -> string {
    let dash_name  = ($ext_name | str replace --all "_" "-")
    let under_name = ($ext_name | str replace --all "-" "_")
    let combos = [
        [$under_name, $under_name],
        [$under_name, $dash_name],
        [$dash_name,  $dash_name],
        [$dash_name,  $under_name],
    ]
    let found = ($combos | each {|pair|
        let p = ($prov_root | path join "extensions/clusters" $pair.0 "default" $"install-($pair.1).sh")
        if ($p | path exists) { $p } else { null }
    } | where { $in != null })
    if ($found | is-empty) { "<not found>" } else { $found | first }
}

# Execute the health gate for an extension, retrying on transient failures.
def cd-health-gate [ext_id: string, gate: record]: nothing -> nothing {
    mut remaining = $gate.retries
    mut passed    = false
    while ($remaining > 0) and ($passed == false) {
        let res = (do { ^bash -c $gate.check_cmd } | complete)
        if $res.exit_code == 0 {
            $passed = true
            print $"  [($ext_id)] health gate OK"
        } else {
            $remaining -= 1
            if $remaining > 0 {
                let attempt = ($gate.retries - $remaining)
                print $"  [($ext_id)] gate ($attempt)/($gate.retries) failed — retry in 10s"
                ^sleep 10
            }
        }
    }
    if $passed == false {
        error make { msg: $"[($ext_id)] health gate failed after ($gate.retries) attempts.\nCmd: ($gate.check_cmd)" }
    }
}

# Deploy cluster extensions — L3 platform or L4 application services.
#
# Reads the deployment DAG from cluster/<cluster>/<layer>-dag.ncl and extension configs
# from cluster/<cluster>/<layer>.ncl. Extensions execute in dependency order defined
# by the DAG `depends_on` arrays. FIP IPs and IDs come from .provisioning-state.json
# written by `provisioning bootstrap`.
#
# Each install script receives:
#   <EXT>_<FIELD>        — scalar config values (namespace, version, host, …)
#   <EXT>_<FIELD>_JSON   — complex config values (ip_pools, node_selector, …)
#   <EXT>_CONFIG_JSON    — full extension config as JSON
#   FIP_A_IP / FIP_A_ID  — FIP-A (Stalwart SMTP)
#   FIP_B_IP / FIP_B_ID  — FIP-B (sgoyol ingress)
#   FIP_C_IP / FIP_C_ID  — FIP-C (wuji)
#   KUBECONFIG           — path to kubeconfig
#
# Usage:
#   provisioning cluster deploy platform sgoyol --workspace librecloud_renew
#   provisioning cluster deploy apps    sgoyol --workspace librecloud_renew
export def "main cluster deploy" [
    layer: string              # Deployment layer: platform | apps
    cluster: string            # Cluster name (e.g. sgoyol, wuji)
    --workspace (-w): string   # Workspace name (default: active workspace)
    --dry-run (-n)              # Print the execution plan without running install scripts
    --kubeconfig (-k): string   # Override KUBECONFIG path for kubectl calls
    --secrets-file (-s): string # SOPS-encrypted dotenv file with install script secrets.
                                # Auto-discovered at cluster/<cluster>/secrets.sops.env if omitted.
] : nothing -> nothing {
    if not ($layer in ["platform", "apps"]) {
        error make { msg: $"layer must be 'platform' or 'apps', got: ($layer)" }
    }

    let ws_name = if ($workspace | is-not-empty) {
        $workspace
    } else {
        let details = (get-active-workspace-details)
        if ($details == null) {
            error make { msg: "No active workspace — pass --workspace or activate one first" }
        }
        $details.name
    }

    let ws_root   = (get-workspace-path $ws_name)
    let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
    let dag_rel   = $"cluster/($cluster)/($layer)-dag.ncl"
    let cfg_rel   = $"cluster/($cluster)/($layer).ncl"
    let kube_cfg  = if ($kubeconfig | is-not-empty) {
        $kubeconfig
    } else {
        $env.KUBECONFIG? | default "/etc/kubernetes/admin.conf"
    }

    print $"Cluster deploy | workspace: ($ws_name) | cluster: ($cluster) | layer: ($layer)"
    if $dry_run { print "DRY RUN — install scripts will not execute" }
    if ($secrets_file | is-not-empty) { print $"  secrets: ($secrets_file)" }
    print ""

    let dag      = (cd-ncl-export $ws_root $dag_rel)
    let cfg      = (cd-ncl-export $ws_root $cfg_rel)
    let fip_env  = (cd-load-fip-env $ws_root)
    let ext_cfgs = ($cfg | get extensions)

    # SOPS secrets: explicit path > auto-discovered cluster/<cluster>/secrets.sops.env > empty.
    # Secrets are merged AFTER NCL env vars — they override any overlapping computed values.
    let secrets_env = if ($secrets_file | is-not-empty) {
        cd-load-secrets $secrets_file
    } else {
        let auto_path = ($ws_root | path join $"cluster/($cluster)/secrets.sops.env")
        if ($auto_path | path exists) {
            print $"  secrets: ($auto_path)"
            cd-load-secrets $auto_path
        } else {
            {}
        }
    }

    # Walk extensions in array order; verify depends_on are satisfied, then install + gate.
    let _completed = ($dag.extensions | reduce --fold [] {|entry, completed|
        let ext_id = $entry.id

        # Dependency guard — catches DAG authoring errors.
        let unsatisfied = ($entry.depends_on | where {|dep|
            ($completed | any {|c| $c == $dep }) == false
        })
        if ($unsatisfied | is-not-empty) {
            error make { msg: $"[($ext_id)] depends on [($unsatisfied | str join ', ')] not yet deployed — fix DAG ordering in ($dag_rel)" }
        }

        # Dispatch: component nodes use extensions/components/ path; extension nodes use extensions/clusters/.
        let is_component = ("component" in $entry) and ($entry | get -o component | default null) != null

        if $is_component {
            let comp     = ($entry.component)
            let comp_name = $comp.name
            let mode      = ($comp | get -o mode | default "cluster")
            let comp_cfg  = ($cfg | get -o components | default {} | get -o $ext_id | default {})
            let env_vars  = (cd-ext-env $comp_name $comp_cfg $fip_env $kube_cfg | merge $secrets_env)

            print $"[($ext_id)] component: ($comp_name) mode=($mode)"
            if ($entry | get -o parallel | default false) { print "  note: parallel=true (sequential execution)" }

            if $dry_run {
                let script_display = (cd-find-component-script-opt $prov_root $comp_name $mode)
                print $"  script:   ($script_display)"
                print $"  env keys: ($env_vars | columns | sort | str join ', ')"
                if ($entry | get -o health_gate | default null) != null {
                    print $"  gate:     ($entry.health_gate.check_cmd | str substring 0..80)..."
                }
            } else {
                let script = (cd-find-component-script $prov_root $comp_name $mode)
                print $"  script: ($script)"
                print ""
                with-env $env_vars { ^bash $script }
                let exit_code = $env.LAST_EXIT_CODE
                if $exit_code != 0 {
                    error make { msg: $"[($ext_id)] component install script exited ($exit_code)" }
                }
                if ($entry | get -o health_gate | default null) != null {
                    cd-health-gate $ext_id $entry.health_gate
                }
            }
        } else {
            let ext_name = $entry.extension
            let ext_cfg  = ($ext_cfgs | get -o $ext_id | default {})
            # secrets_env is merged last — its values win over any NCL-derived env var with the same key.
            let env_vars = (cd-ext-env $ext_name $ext_cfg $fip_env $kube_cfg | merge $secrets_env)

            print $"[($ext_id)] extension: ($ext_name)"
            if ($entry | get -o parallel | default false) { print "  note: parallel=true (sequential execution)" }

            if $dry_run {
                let script_display = (cd-find-script-opt $prov_root $ext_name)
                print $"  script:   ($script_display)"
                print $"  env keys: ($env_vars | columns | sort | str join ', ')"
                if ($entry | get -o health_gate | default null) != null {
                    print $"  gate:     ($entry.health_gate.check_cmd | str substring 0..80)..."
                }
            } else {
                let script = (cd-find-script $prov_root $ext_name)
                print $"  script: ($script)"
                print ""
                with-env $env_vars { ^bash $script }
                let exit_code = $env.LAST_EXIT_CODE
                if $exit_code != 0 {
                    error make { msg: $"[($ext_id)] install script exited ($exit_code)" }
                }
                if ($entry | get -o health_gate | default null) != null {
                    cd-health-gate $ext_id $entry.health_gate
                }
            }
        }

        print ""
        $completed | append $ext_id
    })

    print $"Cluster deploy complete: ($layer) on ($cluster)"
}
