use ../lib_provisioning/user/config.nu [get-workspace-path, get-active-workspace-details]
use ../../../extensions/providers/hetzner/nulib/hetzner/api.nu *
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft]

# Resolve workspace root path.
# Priority: PWD config/provisioning.ncl > convention (pwd-basename) > active workspace > PWD.
def fip-ws-root []: nothing -> string {
    # PWD-based detection first — user is likely in a workspace directory
    let pwd_config = ($env.PWD | path join "config" "provisioning.ncl")
    if ($pwd_config | path exists) {
        return $env.PWD
    }
    # Convention: pwd basename has infra/bootstrap.ncl
    if ($env.PWD | path join "infra" "bootstrap.ncl" | path exists) {
        return $env.PWD
    }
    # Fallback: active workspace
    let details = (do -i { get-active-workspace-details } | default null)
    if $details != null and ($details.name? | is-not-empty) {
        let p = do -i { get-workspace-path $details.name } | default ""
        if ($p | is-not-empty) { return $p }
    }
    $env.PWD
}

# Load FIP role mapping from .provisioning-state.json.
# Returns a record keyed by FIP name → role string.
def load-fip-roles [ws_root: string]: nothing -> record {
    let state_path = ($ws_root | path join ".provisioning-state.json")
    if not ($state_path | path exists) { return {} }

    let fips = (open --raw $state_path | from json | get -o bootstrap.floating_ips | default {})
    $fips | items {|role entry|
        { key: $entry.name, value: $role }
    } | reduce -f {} {|it acc| $acc | insert $it.key $it.value}
}

# Build a server_id → hostname map, cached for 5 minutes in the system temp directory.
# On cache hit: disk read only, no API call. On cache miss: fetch + write cache.
def build-server-map []: nothing -> record {
    let cache_path = ($env.TMPDIR? | default "/tmp" | path join "provisioning_srv_cache.json")

    if ($cache_path | path exists) {
        let age = ((date now) - (ls $cache_path | first | get modified))
        if $age < 5min {
            return (open --raw $cache_path | from json)
        }
    }

    let map = (
        (do -i { hetzner_api_list_servers } | default [])
        | reduce -f {} {|s acc| $acc | insert ($s.id | into string) $s.name}
    )
    $map | to json | save --force $cache_path
    $map
}

# Fetch FIPs then resolve server names from cache or API.
# Server map is cached for 5 min — only FIPs are fetched live on each invocation.
def fetch-fips-and-servers []: nothing -> record {
    let fips    = hetzner_api_list_floating_ips
    let srv_map = build-server-map
    { fips: $fips, srv_map: $srv_map }
}

# Extract location name string from a home_location field (record or string).
def extract-location [loc: any]: nothing -> string {
    if $loc == null { return "" }
    if ($loc | describe) == "string" { return $loc }
    $loc | get -o name | default ""
}

# Extract first dns_ptr string from dns_ptr field (array of {ip, dns_ptr} or string).
def extract-dns-ptr [ptr: any]: nothing -> string {
    if $ptr == null { return "" }
    if ($ptr | describe) == "string" { return $ptr }
    if ($ptr | describe | str starts-with "list") {
        if ($ptr | is-empty) { return "" }
        $ptr | first | get -o dns_ptr | default ""
    } else {
        ""
    }
}

# Format protection record as a short string.
def fmt-prot [prot: any]: nothing -> string {
    if $prot == null { return "—" }
    let d = ($prot | get -o delete  | default false)
    let r = ($prot | get -o rebuild | default false)
    match [$d, $r] {
        [true,  true]  => "del+rbld"
        [true,  false] => "del"
        [false, true]  => "rbld"
        _              => "—"
    }
}

# ── Private helpers ────────────────────────────────────────────────────────────
# These hold the actual logic. Both export def "main *" and def main call them,
# avoiding the Nu parser limitation with quoted-name command calls + flags.

def _fip-list [--out: string]: nothing -> nothing {
    let ws_root   = (fip-ws-root)
    let fip_roles = (load-fip-roles $ws_root)
    let fetched   = (fetch-fips-and-servers)
    let fips      = $fetched.fips
    let srv_map   = $fetched.srv_map

    # Load FIPs declared in bootstrap.ncl (desired state)
    let bootstrap_path = ($ws_root | path join "infra" "bootstrap.ncl")
    let declared_fips = if ($bootstrap_path | path exists) {
        let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
        let data = (ncl-eval-soft $bootstrap_path [$ws_root $prov_root] null)
        if $data != null {
            $data | get -o floating_ips | default [] | each {|f| $f.name}
        } else { [] }
    } else { [] }

    let rows = ($fips | each {|f|
        let role       = ($fip_roles | get -o $f.name | default "—")
        let srv_id     = ($f | get -o server | default null)
        let assigned   = if $srv_id != null { $srv_map | get -o ($srv_id | into string) | default "—" } else { "—" }
        let protection = (fmt-prot ($f | get -o protection | default null))
        {
            name:       $f.name
            ip:         $f.ip
            role:       $role
            location:   (extract-location ($f | get -o home_location | default null))
            assigned:   $assigned
            protection: $protection
            dns_ptr:    (extract-dns-ptr ($f | get -o dns_ptr | default null))
            state:      "created"
        }
    })

    # Add declared-but-not-yet-created FIPs
    let live_names = ($fips | each {|f| $f.name})
    let pending = ($declared_fips | where {|n| not ($live_names | any {|l| $l == $n})}
        | each {|n| {
            name: $n, ip: "—", role: "—", location: "—",
            assigned: "—", protection: "—", dns_ptr: "—", state: "pending bootstrap"
        }}
    )
    let all_rows = ($rows | append $pending)

    match ($out | default "") {
        "json" => { print ($all_rows | to json) }
        "yaml" => { print ($all_rows | to yaml) }
        _ => {
            if ($all_rows | is-empty) {
                print "No floating IPs — declared or created."
            } else {
                print ($all_rows | table -i false)
            }
        }
    }
}

def _fip-show [name: string, --out: string]: nothing -> nothing {
    let ws_root   = (fip-ws-root)
    let fip_roles = (load-fip-roles $ws_root)
    let fetched   = (fetch-fips-and-servers)
    let fips      = $fetched.fips
    let srv_map   = $fetched.srv_map

    let matches = ($fips | where {|f| $f.name == $name or $f.ip == $name })

    # If not in Hetzner, check if declared in bootstrap.ncl
    if ($matches | is-empty) {
        let bootstrap_path = ($ws_root | path join "infra" "bootstrap.ncl")
        let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
        let declared = if ($bootstrap_path | path exists) {
            let data = (ncl-eval-soft $bootstrap_path [$ws_root $prov_root] null)
            if $data != null {
                $data | get -o floating_ips | default [] | where {|f| $f.name == $name}
            } else { [] }
        } else { [] }

        if ($declared | is-empty) {
            error make { msg: $"Floating IP '($name)' not found in Hetzner or bootstrap.ncl" }
        }
        let d = ($declared | first)
        let detail = {
            name:          $d.name
            ip:            "— (not created)"
            state:         "pending bootstrap"
            type:          ($d.type? | default "ipv4")
            home_location: ($d.location? | default "—")
            description:   ($d.description? | default "—")
            labels:        ($d.labels? | default {})
        }
        match ($out | default "") {
            "json" => { print ($detail | to json) }
            "yaml" => { print ($detail | to yaml) }
            _ => {
                print $"\n(ansi yellow)($detail.name)(ansi reset)  [pending bootstrap — not yet in Hetzner]"
                print ($detail | reject name | table -e -i false)
            }
        }
        return
    }

    let f = ($matches | first)
    let role     = ($fip_roles | get -o $f.name | default "—")
    let srv_id   = ($f | get -o server | default null)
    let assigned = if $srv_id != null { $srv_map | get -o ($srv_id | into string) | default "—" } else { "—" }

    let detail = {
        id:            ($f.id | into string)
        name:          $f.name
        ip:            $f.ip
        role:          $role
        type:          ($f | get -o type | default "ipv4")
        home_location: (extract-location ($f | get -o home_location | default null))
        assigned_to:   $assigned
        dns_ptr:       (extract-dns-ptr ($f | get -o dns_ptr | default null))
        protection:    (fmt-prot ($f | get -o protection | default null))
        labels:        ($f | get -o labels | default {})
        state:         "created"
    }

    match ($out | default "") {
        "json" => { print ($detail | to json) }
        "yaml" => { print ($detail | to yaml) }
        _      => {
            print $"\n(ansi cyan_bold)($detail.name)(ansi reset)  ($detail.ip)"
            print ($detail | reject name ip | table -e -i false)
        }
    }
}

def _fip-assign [name: string, server: string, --yes (-y)]: nothing -> nothing {
    let fips = (hetzner_api_list_floating_ips)
    let fip_matches = ($fips | where {|f| $f.name == $name })
    if ($fip_matches | is-empty) {
        error make { msg: $"Floating IP '($name)' not found" }
    }
    let fip    = ($fip_matches | first)
    let fip_id = ($fip.id | into string)

    let srv = (do -i { hetzner_api_server_info $server } | default null)
    if $srv == null {
        error make { msg: $"Server '($server)' not found in Hetzner" }
    }
    let srv_id = ($srv.id | into string)

    let current = ($fip | get -o server | default null)
    if $current != null {
        let current_host = (resolve-server-hostname $current)
        if not $yes {
            print $"FIP ($name) is currently assigned to ($current_host). Reassign to ($server)? [yes/N]"
            let input = (input --numchar 3 | str trim)
            if $input != "yes" { print "Aborted."; return }
        }
        hetzner_api_unassign_floating_ip $fip_id | ignore
    }

    print $"Assigning ($name) [($fip.ip)] → ($server) [($srv_id)] ..."
    hetzner_api_assign_floating_ip $fip_id $srv_id | ignore
    print $"✓ Assigned"
}

def _fip-unassign [name: string, --yes (-y)]: nothing -> nothing {
    let fips = (hetzner_api_list_floating_ips)
    let matches = ($fips | where {|f| $f.name == $name })
    if ($matches | is-empty) {
        error make { msg: $"Floating IP '($name)' not found" }
    }
    let fip    = ($matches | first)
    let fip_id = ($fip.id | into string)

    let srv_id = ($fip | get -o server | default null)
    if $srv_id == null {
        print $"($name) is not assigned to any server — nothing to do."
        return
    }

    let hostname = (resolve-server-hostname $srv_id)
    if not $yes {
        print $"Unassign ($name) [($fip.ip)] from ($hostname)? [yes/N]"
        let input = (input --numchar 3 | str trim)
        if $input != "yes" { print "Aborted."; return }
    }

    print $"Unassigning ($name) from ($hostname) ..."
    hetzner_api_unassign_floating_ip $fip_id | ignore
    print "✓ Unassigned"
}

def _fip-delete [name: string, --yes (-y)]: nothing -> nothing {
    let fips = (hetzner_api_list_floating_ips)
    let matches = ($fips | where {|f| $f.name == $name })
    if ($matches | is-empty) {
        error make { msg: $"Floating IP '($name)' not found" }
    }
    let fip    = ($matches | first)
    let fip_id = ($fip.id | into string)

    let protected = ($fip | get -o protection.delete | default false)
    if $protected {
        error make { msg: $"($name) has delete protection enabled — disable it first with: provisioning fip protection ($name) disable" }
    }

    let srv_id = ($fip | get -o server | default null)
    if $srv_id != null {
        error make { msg: $"($name) is still assigned to a server — unassign it first with: provisioning fip unassign ($name)" }
    }

    if not $yes {
        print $"Delete floating IP ($name) [($fip.ip)] permanently? [yes/N]"
        let input = (input --numchar 3 | str trim)
        if $input != "yes" { print "Aborted."; return }
    }

    print $"Deleting ($name) [($fip.ip)] ..."
    hetzner_api_delete_floating_ip $fip_id | ignore
    print $"✓ Deleted"
}

def _fip-protection [name: string, action: string]: nothing -> nothing {
    let valid = ["enable", "disable"]
    if not ($action in $valid) {
        error make { msg: $"Invalid action '($action)'. Use: enable | disable" }
    }

    let fips = (hetzner_api_list_floating_ips)
    let matches = ($fips | where {|f| $f.name == $name })
    if ($matches | is-empty) {
        error make { msg: $"Floating IP '($name)' not found" }
    }
    let fip    = ($matches | first)
    let fip_id = ($fip.id | into string)
    let enable = ($action == "enable")

    print $"($action | str capitalize)ing delete protection on ($name) ..."
    hetzner_api_floating_ip_change_protection $fip_id $enable | ignore
    print $"✓ Protection ($action)d"
}

# ── Public subcommands (module API) ───────────────────────────────────────────

# List all Floating IPs with role, assigned server, and protection status.
export def "main list" [
    --out: string   # Output format: json | yaml | text (default)
]: nothing -> nothing {
    _fip-list --out ($out | default "")
}

# Show detailed information about a single Floating IP.
export def "main show" [
    name: string    # FIP name or IP address
    --out: string   # Output format: json | yaml | text (default)
]: nothing -> nothing {
    _fip-show $name --out ($out | default "")
}

# Assign a Floating IP to a server (looked up by hostname).
export def "main assign" [
    name:   string  # FIP name
    server: string  # Target server hostname
    --yes (-y)      # Skip confirmation
]: nothing -> nothing {
    if $yes { _fip-assign $name $server --yes } else { _fip-assign $name $server }
}

# Unassign a Floating IP from its current server.
export def "main unassign" [
    name: string    # FIP name
    --yes (-y)      # Skip confirmation
]: nothing -> nothing {
    if $yes { _fip-unassign $name --yes } else { _fip-unassign $name }
}

# Delete a Floating IP permanently. FIP must be unassigned and protection-free.
export def "main delete" [
    name: string    # FIP name
    --yes (-y)      # Skip confirmation
]: nothing -> nothing {
    if $yes { _fip-delete $name --yes } else { _fip-delete $name }
}

# Enable or disable delete protection on a Floating IP.
export def "main protection" [
    name:   string  # FIP name
    action: string  # enable | disable
]: nothing -> nothing {
    _fip-protection $name $action
}

# ── Script entry point ────────────────────────────────────────────────────────
# Active only when fip.nu is run directly (nu fip.nu list).
# Not exported: invisible when fip.nu is `use`d by infrastructure.nu.

def main [
    subcommand?: string   # list | show | assign | unassign | delete | protection
    ...args: string
    --out: string         # Output format: json | yaml | text
    --yes (-y)            # Skip confirmation prompts
]: nothing -> nothing {
    let sub = ($subcommand | default "list")
    if $sub == "list" {
        _fip-list --out ($out | default "")
    } else if $sub == "show" {
        _fip-show ($args | first | default "") --out ($out | default "")
    } else if $sub == "assign" {
        let fip = ($args | get -o 0 | default "")
        let srv = ($args | get -o 1 | default "")
        if $yes { _fip-assign $fip $srv --yes } else { _fip-assign $fip $srv }
    } else if $sub == "unassign" {
        let fip = ($args | first | default "")
        if $yes { _fip-unassign $fip --yes } else { _fip-unassign $fip }
    } else if $sub == "delete" {
        let fip = ($args | first | default "")
        if $yes { _fip-delete $fip --yes } else { _fip-delete $fip }
    } else if $sub == "protection" {
        _fip-protection ($args | get -o 0 | default "") ($args | get -o 1 | default "")
    } else {
        print $"Unknown fip subcommand: ($sub)"
        print "Usage: provisioning fip <list|show|assign|unassign|delete|protection> [args]"
    }
}
