# Platform Service Manager - Service management for deployment
# Handles loading deployment configuration and service management

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

# Normalize service name: strip "provisioning-" or "provisioning_" prefix if present
# Returns the normalized name (e.g., "provisioning_daemon" → "daemon")
export def normalize-service-name [service_name: string] {
  if ($service_name | str starts-with "provisioning-") {
    $service_name | str replace "provisioning-" ""
  } else if ($service_name | str starts-with "provisioning_") {
    $service_name | str replace "provisioning_" ""
  } else {
    $service_name
  }
}

# Load deployment mode configuration from Nickel
export def load-deployment-mode [] {
  let home = ($env.HOME? | default "~" | path expand)

  # Try multiple possible locations for the deployment-mode.ncl file
  let possible_paths = [
    # First try the explicit env var
    ($env.PROVISIONING_USER_PLATFORM? | default null),
    # macOS: Library/Application Support
    ($home | path join "Library/Application Support/provisioning/platform"),
    # Linux/other: .config
    ($home | path join ".config/provisioning/platform"),
  ]

  let config_file = (
    $possible_paths
    | where { |p| $p != null }
    | each { |p| $p | path expand | path join "deployment-mode.ncl" }
    | where { |p| $p | path exists }
    | get 0?
  )

  if ($config_file == null) {
    error make {msg: "Deployment mode file not found in any of the expected locations"}
  }

  let import_path = ($env.PROVISIONING? | default "")
  let import_paths = if ($import_path | is-not-empty) { [$import_path] } else { [] }
  ncl-eval $config_file $import_paths
}

# Load individual service configuration
export def load-service-config [service_name: string] {
  let home = ($env.HOME? | default "~" | path expand)

  # Try multiple possible base paths
  let possible_bases = [
    # First try the explicit env var
    ($env.PROVISIONING_USER_PLATFORM? | default null),
    # macOS: Library/Application Support
    ($home | path join "Library/Application Support/provisioning/platform"),
    # Linux/other: .config
    ($home | path join ".config/provisioning/platform"),
  ]

  # Try to find the config file
  let config_file = (
    $possible_bases
    | where { |b| $b != null }
    | each { |base|
      # Try both the underscore and dash versions
      let base_expanded = ($base | path expand)
      let path1 = ($base_expanded | path join "config" ($"($service_name).ncl"))
      let path2 = ($base_expanded | path join "config" ($"($service_name | str replace "_" "-").ncl"))

      if ($path1 | path exists) {
        $path1
      } else if ($path2 | path exists) {
        $path2
      } else {
        null
      }
    }
    | where { |p| $p != null }
    | get 0?
  )

  if ($config_file == null) {
    return null
  }

  ncl-eval-soft $config_file [] null
}

# Get the port for a service from its configuration
export def get-service-port [service_name: string] {
  let config = (load-service-config $service_name)

  if ($config == null) {
    return "?"
  }

  # Try to extract port from the service configuration
  # Different services store the port in different locations
  let service_key = $service_name | str replace "-" "_"

  if ($config | get --optional $service_key) != null {
    let service_config = ($config | get $service_key)

    # Try common port locations in order
    # 1. server.port (most services: orchestrator, vault_service, etc.)
    if ($service_config | get --optional "server") != null {
      if ($service_config.server | get --optional "port") != null {
        return ($service_config.server.port | into string)
      }
    }

    # 2. build.port (RAG service)
    if ($service_config | get --optional "build") != null {
      if ($service_config.build | get --optional "port") != null {
        return ($service_config.build.port | into string)
      }
    }

    # 3. http.port (some services)
    if ($service_config | get --optional "http") != null {
      if ($service_config.http | get --optional "port") != null {
        return ($service_config.http.port | into string)
      }
    }

    # 4. port at root level
    if ($service_config | get --optional "port") != null {
      return ($service_config.port | into string)
    }
  }

  "?"
}

# Start required services based on deployment configuration
export def start-required-services [] {
  let deployment = (load-deployment-mode)

  # Get enabled services from deployment config
  let services = $deployment.services
  let all_service_names = ($services | columns)

  # Filter to enabled services
  let enabled_services = (
    $all_service_names
    | where {|name|
      let config = ($services | get $name)
      ($config | get --optional "enabled" | default false)
    }
  )

  if ($enabled_services | length) == 0 {
    print "⚠ No services enabled in deployment-mode.ncl"
    return
  }

  # Get current running processes once
  let running_processes = (^ps aux)

  # Start each enabled service
  for service_name in $enabled_services {
    let port = (get-service-port $service_name)
    let normalized_name = (normalize-service-name $service_name)
    let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
    let is_running = ($running_processes | str contains $binary_name)

    # Check if binary exists
    let home = ($env.HOME? | default "~" | path expand)
    let binary_path = ($home | path join ".local/bin" $binary_name)

    if not ($binary_path | path exists) {
      print $"✗ ($service_name) binary not found"
      continue
    }

    # If already running, just report it
    if $is_running {
      let status_msg = $"((ansi green))started((ansi reset))"
      print $"✓ ($service_name) on port ($port) — ($status_msg)"
      continue
    }

    # Start in background
    print $"→ Starting ($service_name)..."

    let log_dir = ($home | path join ".provisioning/logs")
    (do { mkdir ($log_dir) } | ignore)
    let log_file = ($log_dir | path join $"($service_name).log")

    # Set environment variables for service
    let platform_path = ($home | path join "Library/Application Support/provisioning/platform")
    let provisioning_path = ($home | path join ".local/bin/provisioning")

    # Build start command (only orchestrator accepts --provisioning-path)
    let start_cmd = if ($normalized_name == "orchestrator") {
      $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &"
    } else {
      $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) >>\"($log_file)\" 2>&1 &"
    }

    (^sh -c $start_cmd | ignore)
    sleep 2sec

    let started_msg = $"((ansi green))started((ansi reset))"
    print $"✓ ($service_name) on port ($port) — ($started_msg)"
  }

  # ncl-sync: Nickel config cache daemon — always started, independent of deployment mode.
  ncl-sync-start

  print ""
}

# Start specific services by name
# Usage: start-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"]
export def start-services [service_list: list<string>] {
  # Parse service names (handle comma-separated strings)
  let services = (
    $service_list
    | each { |item| $item | split row "," | each { |s| $s | str trim } }
    | flatten
    | where { |s| ($s | is-not-empty) }
  )

  if ($services | length) == 0 {
    print "⚠ No services specified"
    return
  }

  # Get current running processes once
  let running_processes = (^ps aux)

  # Start each service
  for service_name in $services {
    let normalized_name = (normalize-service-name $service_name)
    let port = (get-service-port $normalized_name)
    let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
    let is_running = ($running_processes | str contains $binary_name)

    # Check if binary exists
    let home = ($env.HOME? | default "~" | path expand)
    let binary_path = ($home | path join ".local/bin" $binary_name)

    if not ($binary_path | path exists) {
      print $"✗ ($service_name) binary not found"
      continue
    }

    # If already running, just report it
    if $is_running {
      let status_msg = $"((ansi green))started((ansi reset))"
      print $"✓ ($service_name) on port ($port) — ($status_msg)"
      continue
    }

    # Start in background
    print $"→ Starting ($service_name)..."

    let log_dir = ($home | path join ".provisioning/logs")
    (do { mkdir ($log_dir) } | ignore)
    let log_file = ($log_dir | path join $"($service_name).log")

    # Set environment variables for service
    let platform_path = ($home | path join "Library/Application Support/provisioning/platform")
    let provisioning_path = ($home | path join ".local/bin/provisioning")

    # Build start command (only orchestrator accepts --provisioning-path)
    let start_cmd = if ($normalized_name == "orchestrator") {
      $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &"
    } else {
      $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) >>\"($log_file)\" 2>&1 &"
    }

    (^sh -c $start_cmd | ignore)
    sleep 2sec

    let started_msg = $"((ansi green))started((ansi reset))"
    print $"✓ ($service_name) on port ($port) — ($started_msg)"
  }

  print ""
}

# Stop specific services by name
# Usage: stop-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"]
export def stop-services [service_list: list<string>] {
  # Parse service names (handle comma-separated strings)
  let services = (
    $service_list
    | each { |item| $item | split row "," | each { |s| $s | str trim } }
    | flatten
    | where { |s| ($s | is-not-empty) }
  )

  if ($services | length) == 0 {
    print "⚠ No services specified"
    return
  }

  # Get current running processes once
  let running_processes = (^ps aux)

  # Stop each service
  for service_name in $services {
    # Normalize service name: strip "provisioning-" or "provisioning_" prefix if present
    let normalized_name = (
      if ($service_name | str starts-with "provisioning-") {
        $service_name | str replace "provisioning-" ""
      } else if ($service_name | str starts-with "provisioning_") {
        $service_name | str replace "provisioning_" ""
      } else {
        $service_name
      }
    )

    let port = (get-service-port $normalized_name)
    let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
    let is_running = ($running_processes | str contains $binary_name)

    if $is_running {
      (^sh -c $"pkill -f '($binary_name)' 2>/dev/null || true") | ignore
      sleep 500ms
      let stopped_msg = $"((ansi red))stopped((ansi reset))"
      print $"✓ ($service_name) on port ($port) — ($stopped_msg)"
    } else {
      let stopped_msg = $"((ansi red))already stopped((ansi reset))"
      print $"✓ ($service_name) on port ($port) — ($stopped_msg)"
    }
  }

  print ""
}

# Check if a port is listening (health check for external services)
export def is-port-listening [port: number] {
  let uname_result = (do { ^uname -s } | complete)
  let os_type = (if $uname_result.exit_code == 0 { $uname_result.stdout | str trim } else { "Linux" })

  if $os_type == "Darwin" {
    # macOS: use lsof to check listening ports
    # Pattern matches both "*:PORT" and "127.0.0.1:PORT" formats
    let check = (do { ^lsof -i -P -n } | complete)
    if $check.exit_code == 0 {
      let port_str = $"($port)"
      $check.stdout | str contains $"($port_str)" | if $in { true } else { false }
    } else {
      false
    }
  } else {
    # Linux: use netstat to check listening ports
    let check = (do { ^netstat -tuln } | complete)
    if $check.exit_code == 0 {
      $check.stdout | str contains $":$port"
    } else {
      false
    }
  }
}

# Get external services from user configuration file
export def get-external-services [] {
  let home = ($env.HOME? | default "~" | path expand)

  # Try multiple possible base paths for external services config
  let possible_bases = [
    # First try the explicit env var
    ($env.PROVISIONING_USER_PLATFORM? | default null),
    # macOS: Library/Application Support
    ($home | path join "Library/Application Support/provisioning/platform"),
    # Linux/other: .config
    ($home | path join ".config/provisioning/platform"),
  ]

  # Try to find the external services config file
  let external_services_file = (
    $possible_bases
    | where { |b| $b != null }
    | each { |base| ($base | path expand | path join "config/external-services.ncl") }
    | where { |p| $p | path exists }
    | get 0?
  )

  if ($external_services_file == null) {
    return []
  }

  ncl-eval-soft $external_services_file [] [] | default []
}

# Start nats-server as a child process for solo mode.
# Requires nats-server to be in PATH.
# Returns a record with {pid, port, jetstream_dir} on success.
export def nats_start [config: record]: nothing -> record {
  let port = ($config.port? | default 4222)
  let data_dir = ($env.HOME | path join ".local/share/provisioning/nats")
  let js_dir = ($config.jetstream_store_dir? | default $data_dir)

  let mk_result = (do { ^mkdir -p $js_dir } | complete)
  if ($mk_result.exit_code != 0) {
    error make {msg: $"Failed to create NATS data dir ($js_dir): ($mk_result.stderr)"}
  }

  # Spawn nats-server in background — nohup keeps it alive after this shell exits
  let cmd = $"nohup nats-server -js -sd ($js_dir) -p ($port) >/dev/null 2>&1 &"
  let start_result = (do { ^sh -c $cmd } | complete)
  if ($start_result.exit_code != 0) {
    error make {msg: $"nats-server failed to start: ($start_result.stderr)"}
  }

  # Poll for readiness — up to 10 seconds in 500ms increments
  let ready = (
    1..20
    | each { |_i|
      sleep 500ms
      (nats_health {port: $port})
    }
    | where { |r| $r }
    | length
    | $in > 0
  )

  if (not $ready) {
    error make {msg: "nats-server did not become ready within 10 seconds"}
  }

  let pid_result = (do { ^pgrep -f $"nats-server" } | complete)
  let pid = (if ($pid_result.exit_code == 0) { $pid_result.stdout | lines | get 0? | default "0" | into int } else { 0 })

  {pid: $pid, port: $port, jetstream_dir: $js_dir}
}

# Stop the nats-server process.
export def nats_stop [config: record]: nothing -> nothing {
  let port = ($config.port? | default 4222)
  let kill_result = (do { ^pkill -f "nats-server" } | complete)
  if ($kill_result.exit_code != 0) {
    print $"Warning: nats-server on port ($port) was not running or could not be stopped"
  }
}

# Check if nats-server is accepting TCP connections on the configured port.
# Returns true if healthy, false otherwise.
export def nats_health [config: record]: nothing -> bool {
  let port = ($config.port? | default 4222)
  let check = (do { ^nc -z -w 1 127.0.0.1 $port } | complete)
  $check.exit_code == 0
}

# ============================================================================
# ncl-sync daemon management
# ============================================================================

def ncl-sync-cache-dir []: nothing -> string {
  if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR }
  # Walk up from PWD to find workspace root
  let pwd = $env.PWD
  let ws_pwd = if ($pwd | path join "infra" | path exists) or ($pwd | path join "config" "provisioning.ncl" | path exists) or ($pwd | path join ".ontology" | path exists) {
    $pwd
  } else { "" }
  if ($ws_pwd | is-not-empty) { return ($ws_pwd | path join ".ncl-cache") }
  # Fallback to active workspace from user_config
  let details = (get-active-workspace-details)
  if ($details | is-not-empty) and ($details | get -o path | is-not-empty) {
    return ($details.path | path join ".ncl-cache")
  }
  let home = ($env.HOME? | default "~" | path expand)
  $home | path join ".cache" "provisioning" "config-cache"
}

def ncl-sync-pid-file []: nothing -> string {
  (ncl-sync-cache-dir) | path join "ncl-sync.pid"
}

def ncl-sync-bin []: nothing -> string {
  let home = ($env.HOME? | default "~" | path expand)
  $home | path join ".local" "bin" "provisioning-ncl-sync"
}

def ncl-sync-running []: nothing -> bool {
  let pid_file = (ncl-sync-pid-file)
  if not ($pid_file | path exists) { return false }
  let pid = (open $pid_file | str trim)
  if ($pid | is-empty) { return false }
  let check = (do { ^kill -0 ($pid | into int) } | complete)
  $check.exit_code == 0
}

# Check that a path has workspace markers (infra/, config/provisioning.ncl, or .ontology/).
def is-workspace-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 `path` until a workspace root is found or we reach filesystem root.
def find-workspace-up [path: string]: nothing -> string {
  if ($path | is-empty) or $path == "/" { return "" }
  if (is-workspace-dir $path) { return $path }
  let parent = ($path | path dirname)
  if $parent == $path { return "" }
  find-workspace-up $parent
}

# Start the ncl-sync daemon if not already running.
# Workspace resolution priority:
#   1. $NCL_CACHE_DIR's parent (explicit override)
#   2. Walk up from PWD until a workspace root is found
#   3. get-active-workspace-details from user_config.yaml (if it's a valid workspace path)
#   4. skip (avoid watching HOME or random dirs)
export def ncl-sync-start []: nothing -> nothing {
  if (ncl-sync-running) { return }
  let bin = (ncl-sync-bin)
  if not ($bin | path exists) { return }

  let from_pwd = (find-workspace-up $env.PWD)
  let details = (get-active-workspace-details)
  let from_config = if ($details | is-not-empty) and ($details | get -o path | is-not-empty) {
    $details.path
  } else { "" }

  let ws_path = if ($from_pwd | is-not-empty) {
    $from_pwd
  } else if (is-workspace-dir $from_config) {
    $from_config
  } else {
    ""
  }

  if ($ws_path | is-empty) {
    print "→ ncl-sync: no workspace detected in PWD tree or user_config — skipping"
    return
  }

  let home = ($env.HOME? | default "~" | path expand)
  let log_dir = ($home | path join ".provisioning" "logs")
  (do { mkdir $log_dir } | ignore)
  let log_file = ($log_dir | path join "ncl-sync.log")

  let cmd = $"nohup ($bin) daemon --workspace \"($ws_path)\" >>\"($log_file)\" 2>&1 &"
  (^sh -c $cmd | ignore)
  sleep 500ms
  print $"→ ncl-sync started \(workspace: ($ws_path)\)"
}

# Stop the ncl-sync daemon via PID file.
export def ncl-sync-stop []: nothing -> nothing {
  let pid_file = (ncl-sync-pid-file)
  if not ($pid_file | path exists) { return }
  let pid = (open $pid_file | str trim)
  if ($pid | is-not-empty) {
    (do { ^kill ($pid | into int) } | complete | ignore)
  }
  (do { rm -f $pid_file } | ignore)
}

# Status record for ncl-sync daemon.
export def ncl-sync-status []: nothing -> record {
  let running = (ncl-sync-running)
  let pid_file = (ncl-sync-pid-file)
  let pid = if ($pid_file | path exists) { open $pid_file | str trim } else { "" }
  {
    service: "ncl-sync",
    running: $running,
    pid: $pid,
    pid_file: $pid_file,
  }
}
