## Background heartbeat thread
##
## Nim threads don't share memory by default.
## Use channels for communication between main and heartbeat threads.
##
## Key patterns:
## - Dedicated thread with own SQLite connection
## - Channel for stop signal and status updates
## - Non-blocking tryRecv to check for commands

import std/os
import tiny_sqlite
import ./types
import ./db

type
  HeartbeatCmd* = enum
    hbUpdateStatus
    hbStop

  HeartbeatMsg* = object
    cmd*: HeartbeatCmd
    status*: HeartbeatStatus
    task*: string
    progress*: float

  HeartbeatThread* = ref object
    ## GC-managed heartbeat thread handle
    thread: Thread[HeartbeatArgs]
    channel: Channel[HeartbeatMsg]
    running: bool

  HeartbeatArgs = object
    dbPath: string
    agentId: string
    channelPtr: ptr Channel[HeartbeatMsg]

proc heartbeatWorker(args: HeartbeatArgs) {.thread.} =
  ## Dedicated heartbeat thread - owns its own DB connection
  var db: DbConn
  try:
    db = openBusDb(args.dbPath)
  except CatchableError as e:
    stderr.writeLine("Heartbeat: failed to open database: ", e.msg)
    return

  var status = hsIdle
  var task = ""
  var progress = 0.0

  while true:
    # Check for commands (non-blocking)
    let tried = args.channelPtr[].tryRecv()
    if tried.dataAvailable:
      case tried.msg.cmd
      of hbStop:
        db.close()
        return
      of hbUpdateStatus:
        status = tried.msg.status
        task = tried.msg.task
        progress = tried.msg.progress

    # Write heartbeat
    try:
      db.writeHeartbeat(args.agentId, status, task, progress)
    except CatchableError as e:
      stderr.writeLine("Heartbeat error: ", e.msg)

    sleep(HeartbeatIntervalMs)

proc startHeartbeat*(dbPath, agentId: string): HeartbeatThread =
  ## Start the heartbeat thread. Returns handle for control.
  ## The returned ref is GC-managed - no manual cleanup needed.
  result = HeartbeatThread()
  result.channel.open()
  result.running = true

  let args = HeartbeatArgs(
    dbPath: dbPath,
    agentId: agentId,
    channelPtr: addr result.channel
  )

  createThread(result.thread, heartbeatWorker, args)

proc updateStatus*(hb: HeartbeatThread, status: HeartbeatStatus,
                   task: string = "", progress: float = 0.0) =
  ## Update the status reported in heartbeats
  if hb != nil and hb.running:
    hb.channel.send(HeartbeatMsg(
      cmd: hbUpdateStatus,
      status: status,
      task: task,
      progress: progress
    ))

proc stopHeartbeat*(hb: HeartbeatThread) =
  ## Stop the heartbeat thread and clean up
  if hb != nil and hb.running:
    hb.channel.send(HeartbeatMsg(cmd: hbStop))
    joinThread(hb.thread)
    hb.channel.close()
    hb.running = false
    # No dealloc needed - GC handles ref type

# Simpler API using global state (for single-threaded CLI usage)
var globalHeartbeat: HeartbeatThread = nil

proc startGlobalHeartbeat*(dbPath, agentId: string): bool =
  ## Start heartbeat using global state (simpler API for CLI).
  ## Returns true if started, false if already running.
  if globalHeartbeat != nil:
    return false  # Already running
  globalHeartbeat = startHeartbeat(dbPath, agentId)
  return true

proc updateGlobalHeartbeat*(status: HeartbeatStatus, task: string = "",
                            progress: float = 0.0) =
  ## Update global heartbeat status
  updateStatus(globalHeartbeat, status, task, progress)

proc stopGlobalHeartbeat*() =
  ## Stop global heartbeat
  if globalHeartbeat != nil:
    stopHeartbeat(globalHeartbeat)
    globalHeartbeat = nil
