## Common utilities for worker coordination
##
## Consolidates repeated patterns across modules.

import std/[strformat, strutils, times, options, os, osproc]
import tiny_sqlite
import ./types

# Path helpers
proc branchName*(taskId: string): string =
  ## Get the feature branch name for a task
  &"feat/{taskId}"

proc findMainRepoDir*(): string =
  ## Find the main repository directory, even when running from a worktree.
  ## Uses git to find the common git directory, then derives the main repo.
  ## Returns current directory if git fails or we're already in main repo.
  let (output, exitCode) = execCmdEx("git rev-parse --git-common-dir")
  if exitCode != 0:
    return getCurrentDir()

  let gitCommonDir = output.strip()
  if gitCommonDir == ".git":
    # Already in main repo
    return getCurrentDir()

  # gitCommonDir is something like "/path/to/main-repo/.git"
  # The main repo is its parent directory
  let mainGitDir = if gitCommonDir.isAbsolute: gitCommonDir
                   else: getCurrentDir() / gitCommonDir
  result = parentDir(mainGitDir)

proc worktreePath*(taskId: string): string =
  ## Get the absolute worktree path for a task.
  ## Uses repo root to ensure path works from any directory.
  findMainRepoDir() / WorktreesDir / taskId

proc getMainRepoBusDbPath*(): string =
  ## Get the absolute path to bus.db in the main repository.
  ## Works correctly even when called from a worktree.
  findMainRepoDir() / BusDbPath

# Time conversion
proc msToTime*(ms: int64): Time =
  ## Convert epoch milliseconds to Time
  fromUnix(ms div 1000)

# Input validation
const AllowedTaskIdChars = {'a'..'z', 'A'..'Z', '0'..'9', '_', '-'}

proc validateTaskId*(taskId: string): string =
  ## Validate task ID to prevent path traversal and injection.
  ## Returns the taskId if valid, raises ValueError otherwise.
  if taskId.len == 0:
    raise newException(ValueError, "Task ID cannot be empty")
  if taskId.len > 64:
    raise newException(ValueError, "Task ID too long (max 64 chars)")
  for c in taskId:
    if c notin AllowedTaskIdChars:
      raise newException(ValueError, "Task ID contains invalid character: " & $c)
  return taskId

# SQLite helpers
proc optString*(val: DbValue): Option[string] =
  ## Convert nullable DbValue to Option[string]
  if val.kind == sqliteNull:
    none(string)
  else:
    some(val.fromDbValue(string))

proc optInt64*(val: DbValue): Option[int64] =
  ## Convert nullable DbValue to Option[int64]
  if val.kind == sqliteNull:
    none(int64)
  else:
    some(val.fromDbValue(int64))

proc optFromDb*[T](val: DbValue, _: typedesc[T]): Option[T] =
  ## Generic helper to convert nullable DbValue to Option[T]
  if val.kind == sqliteNull:
    none(T)
  else:
    some(val.fromDbValue(T))

# Transaction helper
template withTransaction*(db: DbConn, body: untyped) =
  ## Execute body within a transaction with automatic rollback on error.
  db.exec("BEGIN IMMEDIATE")
  try:
    body
    db.exec("COMMIT")
  except CatchableError:
    db.exec("ROLLBACK")
    raise

# Error handling helpers
proc logError*(operation, message: string) =
  ## Log error to stderr with consistent format
  stderr.writeLine("ERROR: ", operation, ": ", message)

proc logWarn*(operation, message: string) =
  ## Log warning to stderr with consistent format
  stderr.writeLine("WARN: ", operation, ": ", message)

template wrapError*(ExType: typedesc, operation: string, body: untyped) =
  ## Wrap an operation, catching errors and re-raising with context.
  ## Usage: wrapError(DbError, "openBusDb"): db = openDatabase(path)
  try:
    body
  except CatchableError as e:
    raise newException(ExType, operation & ": " & e.msg)
