// cell log - Manage and read log sinks
//
// Usage:
//   cell log list                         List configured sinks
//   cell log add <name> console [opts]    Add a console sink
//   cell log add <name> file <path> [opts] Add a file sink
//   cell log remove <name>                Remove a sink
//   cell log read <sink> [opts]           Read from a file sink
//   cell log tail <sink> [--lines=N]      Follow a file sink

var toml = use('toml')
var fd = use('fd')
var json = use('json')

var log_path = shop_path + '/log.toml'

function load_config() {
  if (fd.is_file(log_path)) {
    return toml.decode(text(fd.slurp(log_path)))
  }
  return null
}

function ensure_dir(path) {
  if (fd.is_dir(path)) return
  var parts = array(path, '/')
  var current = starts_with(path, '/') ? '/' : ''
  var i = 0
  for (i = 0; i < length(parts); i++) {
    if (parts[i] == '') continue
    current = current + parts[i] + '/'
    if (!fd.is_dir(current)) fd.mkdir(current)
  }
}

function save_config(config) {
  ensure_dir(shop_path)
  fd.slurpwrite(log_path, stone(blob(toml.encode(config))))
}

function print_help() {
  log.console("Usage: cell log <command> [options]")
  log.console("")
  log.console("Commands:")
  log.console("  list                             List configured sinks")
  log.console("  add <name> console [opts]        Add a console sink")
  log.console("  add <name> file <path> [opts]    Add a file sink")
  log.console("  remove <name>                    Remove a sink")
  log.console("  read <sink> [opts]               Read from a file sink")
  log.console("  tail <sink> [--lines=N]          Follow a file sink")
  log.console("")
  log.console("Options for add:")
  log.console("  --format=pretty|bare|json   Output format (default: pretty for console, json for file)")
  log.console("  --channels=ch1,ch2          Channels to subscribe (default: console,error,system)")
  log.console("  --exclude=ch1,ch2           Channels to exclude (for wildcard sinks)")
  log.console("")
  log.console("Options for read:")
  log.console("  --lines=N                   Show last N lines (default: all)")
  log.console("  --channel=X                 Filter by channel")
  log.console("  --since=timestamp           Only show entries after timestamp")
}

function parse_opt(arg, prefix) {
  var full = '--' + prefix + '='
  if (starts_with(arg, full))
    return text(arg, length(full), length(arg))
  return null
}

function format_entry(entry) {
  var aid = text(entry.actor_id, 0, 5)
  var src = ""
  var ev = null
  if (entry.source && entry.source.file)
    src = entry.source.file + ":" + text(entry.source.line)
  ev = is_text(entry.event) ? entry.event : json.encode(entry.event)
  return "[" + aid + "] [" + entry.channel + "] " + src + " " + ev
}

function do_list() {
  var config = load_config()
  var names = null
  if (!config || !config.sink) {
    log.console("No log sinks configured.")
    log.console("Default: console pretty for console/error/system")
    return
  }
  names = array(config.sink)
  arrfor(names, function(n) {
    var s = config.sink[n]
    var ch = is_array(s.channels) ? text(s.channels, ', ') : '(none)'
    var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
    var fmt = s.format || (s.type == 'file' ? 'json' : 'pretty')
    if (s.type == 'file')
      log.console("  " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex)
    else
      log.console("  " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex)
  })
}

function do_add() {
  var name = null
  var sink_type = null
  var path = null
  var format = null
  var channels = ["console", "error", "system"]
  var exclude = null
  var config = null
  var val = null
  var i = 0
  if (length(args) < 3) {
    log.error("Usage: cell log add <name> console|file [path] [options]")
    return
  }
  name = args[1]
  sink_type = args[2]

  if (sink_type == 'file') {
    if (length(args) < 4) {
      log.error("Usage: cell log add <name> file <path> [options]")
      return
    }
    path = args[3]
    format = "json"
    i = 4
  } else if (sink_type == 'console') {
    format = "pretty"
    i = 3
  } else {
    log.error("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
    return
  }

  for (i = i; i < length(args); i++) {
    val = parse_opt(args[i], 'format')
    if (val) { format = val; continue }
    val = parse_opt(args[i], 'channels')
    if (val) { channels = array(val, ','); continue }
    val = parse_opt(args[i], 'exclude')
    if (val) { exclude = array(val, ','); continue }
  }

  config = load_config()
  if (!config) config = {}
  if (!config.sink) config.sink = {}

  config.sink[name] = {type: sink_type, format: format, channels: channels}
  if (path) config.sink[name].path = path
  if (exclude) config.sink[name].exclude = exclude

  save_config(config)
  log.console("Added sink: " + name)
}

function do_remove() {
  var name = null
  var config = null
  if (length(args) < 2) {
    log.error("Usage: cell log remove <name>")
    return
  }
  name = args[1]
  config = load_config()
  if (!config || !config.sink || !config.sink[name]) {
    log.error("Sink not found: " + name)
    return
  }
  config.sink[name] = null
  save_config(config)
  log.console("Removed sink: " + name)
}

function do_read() {
  var name = null
  var max_lines = 0
  var filter_channel = null
  var since = 0
  var config = null
  var sink = null
  var content = null
  var lines = null
  var entries = []
  var entry = null
  var val = null
  var i = 0

  if (length(args) < 2) {
    log.error("Usage: cell log read <sink_name> [options]")
    return
  }
  name = args[1]

  for (i = 2; i < length(args); i++) {
    val = parse_opt(args[i], 'lines')
    if (val) { max_lines = number(val); continue }
    val = parse_opt(args[i], 'channel')
    if (val) { filter_channel = val; continue }
    val = parse_opt(args[i], 'since')
    if (val) { since = number(val); continue }
  }

  config = load_config()
  if (!config || !config.sink || !config.sink[name]) {
    log.error("Sink not found: " + name)
    return
  }
  sink = config.sink[name]
  if (sink.type != 'file') {
    log.error("Can only read from file sinks")
    return
  }
  if (!fd.is_file(sink.path)) {
    log.console("Log file does not exist yet: " + sink.path)
    return
  }

  content = text(fd.slurp(sink.path))
  lines = array(content, '\n')

  arrfor(lines, function(line) {
    var parse_fn = null
    if (length(line) == 0) return
    parse_fn = function() {
      entry = json.decode(line)
    } disruption {
      entry = null
    }
    parse_fn()
    if (!entry) return
    if (filter_channel && entry.channel != filter_channel) return
    if (since > 0 && entry.timestamp < since) return
    entries[] = entry
  })

  if (max_lines > 0 && length(entries) > max_lines)
    entries = array(entries, length(entries) - max_lines, length(entries))

  arrfor(entries, function(e) {
    log.console(format_entry(e))
  })
}

function do_tail() {
  var name = null
  var tail_lines = 10
  var config = null
  var sink = null
  var last_size = 0
  var val = null
  var i = 0

  if (length(args) < 2) {
    log.error("Usage: cell log tail <sink_name> [--lines=N]")
    return
  }
  name = args[1]

  for (i = 2; i < length(args); i++) {
    val = parse_opt(args[i], 'lines')
    if (val) { tail_lines = number(val); continue }
  }

  config = load_config()
  if (!config || !config.sink || !config.sink[name]) {
    log.error("Sink not found: " + name)
    return
  }
  sink = config.sink[name]
  if (sink.type != 'file') {
    log.error("Can only tail file sinks")
    return
  }
  if (!fd.is_file(sink.path))
    log.console("Waiting for log file: " + sink.path)

  function poll() {
    var st = null
    var poll_content = null
    var poll_lines = null
    var start = 0
    var poll_entry = null
    var old_line_count = 0
    var idx = 0
    var parse_fn = null
    if (!fd.is_file(sink.path)) {
      $delay(poll, 1)
      return
    }
    st = fd.stat(sink.path)
    if (st.size == last_size) {
      $delay(poll, 1)
      return
    }

    poll_content = text(fd.slurp(sink.path))
    poll_lines = array(poll_content, '\n')

    if (last_size == 0 && length(poll_lines) > tail_lines) {
      start = length(poll_lines) - tail_lines
    } else if (last_size > 0) {
      old_line_count = length(array(text(poll_content, 0, last_size), '\n'))
      start = old_line_count
    }

    last_size = st.size
    for (idx = start; idx < length(poll_lines); idx++) {
      if (length(poll_lines[idx]) == 0) continue
      parse_fn = function() {
        poll_entry = json.decode(poll_lines[idx])
      } disruption {
        poll_entry = null
      }
      parse_fn()
      if (!poll_entry) continue
      os.print(format_entry(poll_entry) + "\n")
    }
    $delay(poll, 1)
  }

  poll()
}

// Main dispatch
if (length(args) == 0) {
  print_help()
} else if (args[0] == 'help' || args[0] == '-h' || args[0] == '--help') {
  print_help()
} else if (args[0] == 'list') {
  do_list()
} else if (args[0] == 'add') {
  do_add()
} else if (args[0] == 'remove') {
  do_remove()
} else if (args[0] == 'read') {
  do_read()
} else if (args[0] == 'tail') {
  do_tail()
} else {
  log.error("Unknown command: " + args[0])
  print_help()
}

$stop()
