#!/usr/bin/env ruby
# frozen_string_literal: true

$LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
require 'gli'
require 'doing'
require 'tempfile'
require 'pp'

def class_exists?(class_name)
  klass = Module.const_get(class_name)
  klass.is_a?(Class)
rescue NameError
  false
end

if class_exists? 'Encoding'
  Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external')
  Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal')
end

include GLI::App
include Doing::Errors
version Doing::VERSION
hide_commands_without_desc true
autocomplete_commands true

REGEX_BOOL = /^(?:and|all|any|or|not|none)$/i
REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i

InvalidExportType = Class.new(RuntimeError)
MissingConfigFile = Class.new(RuntimeError)

colors = Doing::Color
wwid = Doing::WWID.new

Doing.logger.log_level = :info

if ENV['DOING_LOG_LEVEL'] || ENV['DOING_DEBUG'] || ENV['DOING_QUIET'] || ENV['DOING_VERBOSE'] || ENV['DOING_PLUGIN_DEBUG']
  # Quiet always wins
  if ENV['DOING_QUIET'] && ENV['DOING_QUIET'].truthy?
    Doing.logger.log_level = :error
  elsif (ENV['DOING_PLUGIN_DEBUG'] && ENV['DOING_PLUGIN_DEBUG'].truthy?)
    Doing.logger.log_level = :debug
  elsif (ENV['DOING_DEBUG'] && ENV['DOING_DEBUG'].truthy?)
    Doing.logger.log_level = :debug
  elsif ENV['DOING_LOG_LEVEL']
    Doing.logger.log_level = ENV['DOING_LOG_LEVEL']
  end
end

if ENV['DOING_CONFIG']
  Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true })
end

config = Doing.config
settings = config.settings
wwid.config = settings

if settings.dig('plugins', 'command_path')
  commands_from File.expand_path(settings.dig('plugins', 'command_path'))
end

program_desc 'A CLI for a What Was I Doing system'
program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text
record of what you've been doing, complete with tag-based time tracking. The
command line tool allows you to add entries, annotate with tags and notes, and
view your entries with myriad options, with a focus on a "natural" language syntax.)

default_command :recent
# sort_help :manually

desc 'Output notes if included in the template'
switch [:notes], default_value: true, negatable: true

desc 'Send results report to STDOUT instead of STDERR'
switch [:stdout], default_value: false, negatable: false

desc 'Use a pager when output is longer than screen'
switch %i[p pager], default_value: settings['paginate']

desc 'Answer yes/no menus with default option'
switch [:default], default_value: false

desc 'Exclude auto tags and default tags'
switch %i[x noauto], default_value: false, negatable: false

desc 'Colored output'
switch %i[color], default_value: true

desc 'Silence info messages'
switch %i[q quiet], default_value: false, negatable: false

desc 'Verbose output'
switch %i[debug], default_value: false, negatable: false

desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead.'
flag [:config_file], default_value: config.config_file

desc 'Specify a different doing_file'
flag %i[f doing_file]

desc 'Add an entry'
long_desc %(Record what you're starting now, or backdate the start time using natural language.

A parenthetical at the end of the entry will be converted to a note.

Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
arg_name 'ENTRY'
command %i[now next] do |c|
  c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
  c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
  c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
  c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
  c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section]

  c.desc "Edit entry with #{Doing::Util.default_editor}"
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc 'Backdate start time [4pm|20m|2h|yesterday noon]'
  c.arg_name 'DATE_STRING'
  c.flag %i[b back]

  c.desc 'Timed entry, marks last entry in section as @done'
  c.switch %i[f finish_last], negatable: false, default_value: false

  c.desc 'Include a note'
  c.arg_name 'TEXT'
  c.flag %i[n note]

  # c.desc "Edit entry with specified app"
  # c.arg_name 'editor_app'
  # # c.flag [:a, :app]

  c.action do |_global_options, options, args|
    if options[:back]
      date = wwid.chronify(options[:back], guess: :begin)

      raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
    else
      date = Time.now
    end

    if options[:section]
      section = wwid.guess_section(options[:section]) || options[:section].cap_first
    else
      options[:section] = settings['current_section']
    end

    if options[:e] || (args.empty? && $stdin.stat.size.zero?)
      raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?

      input = ''
      input += args.join(' ') unless args.empty?
      input = wwid.fork_editor(input).strip

      raise EmptyInput, 'No content' if input.empty?

      title, note = wwid.format_input(input)
      note.push(options[:n]) if options[:n]
      wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
      wwid.write(wwid.doing_file)
    elsif args.length.positive?
      title, note = wwid.format_input(args.join(' '))
      note.push(options[:n]) if options[:n]
      wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
      wwid.write(wwid.doing_file)
    elsif $stdin.stat.size.positive?
      input = $stdin.read
      title, note = wwid.format_input(input)
      note.push(options[:n]) if options[:n]
      wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
      wwid.write(wwid.doing_file)
    else
      raise EmptyInput, 'You must provide content when creating a new entry'
    end
  end
end

desc 'Reset the start time of an entry'
command %i[reset begin] do |c|
  c.desc 'Limit search to section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Resume entry (remove @done)'
  c.switch %i[r resume], default_value: true

  c.desc 'Reset last entry matching tag'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Reset items that *don\'t* match search/tag filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Select from a menu of matching entries'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |global_options, options, args|
    options[:fuzzy] = false
    if options[:section]
      options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
    end

    options[:bool] = options[:bool].normalize_bool

    options[:case] = options[:case].normalize_case

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
      options[:search] = search
    end


    items = wwid.filter_items([], opt: options)

    if options[:interactive]
      last_entry = wwid.choose_from_items(items, {
        menu: true,
        header: '',
        prompt: 'Select an entry to start/reset > ',
        multiple: false,
        sort: false,
        show_if_single: true
      }, include_section: options[:section].nil? )
    else
      last_entry = items.last
    end

    unless last_entry
      Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
      return
    end

    wwid.reset_item(last_entry, resume: options[:resume])

    # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)

    wwid.write(wwid.doing_file)
  end
end


desc 'Add a note to the last entry'
long_desc %(
  If -r is provided with no other arguments, the last note is removed.
  If new content is specified through arguments or STDIN, any previous
  note will be replaced with the new one.

  Use -e to load the last entry in a text editor where you can append a note.
)
arg_name 'NOTE_TEXT'
command :note do |c|
  c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
  c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
  c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
  c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc "Edit entry with #{Doing::Util.default_editor}"
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc "Replace/Remove last entry's note (default append)"
  c.switch %i[r remove], negatable: false, default_value: false

  c.desc 'Add/remove note from last entry matching tag'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Add/remove note from last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Add note to item that *doesn\'t* match search/tag filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Select item for new note from a menu of matching entries'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    if options[:section]
      options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
    end

    options[:tag_bool] = options[:bool].normalize_bool

    options[:case] = options[:case].normalize_case

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
      options[:search] = search
    end


    last_entry = wwid.last_entry(options)

    unless last_entry
      Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
      return
    end

    last_note = last_entry.note || Doing::Note.new
    new_note = Doing::Note.new

    if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
      raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?

      input = !args.empty? ? args.join(' ') : ''

      if options[:remove]
        prev_input = Doing::Note.new
      else
        prev_input = last_entry.note || Doing::Note.new
      end

      input = prev_input.add(input)

      input = wwid.fork_editor([last_entry.title, '### Edit below this line', input.to_s].join("\n")).strip
      _title, note = wwid.format_input(input)
      options[:remove] = true
      new_note.add(note)
    elsif !args.empty?
      new_note.add(args.join(' '))
    elsif $stdin.stat.size.positive?
      new_note.add($stdin.read)
    else
      raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
    end

    if last_note.equal?(new_note)
      Doing.logger.debug('Skipped:', 'No note change')
    else
      last_note.add(new_note, replace: options[:remove])
      Doing.logger.info('Entry updated:', last_entry.title)
    end
    # new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)

    wwid.write(wwid.doing_file)
  end
end

desc 'Finish any running @meanwhile tasks and optionally create a new one'
arg_name 'ENTRY'
command :meanwhile do |c|
  c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
  c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
  c.example 'doing meanwhile --archive', desc: 'Finish any open @meanwhile entry and archive it'
  c.example 'doing meanwhile --back 2h "Something I\'ve been working on for a while', desc: 'Add a @meanwhile entry with a start date 2 hours ago'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section]

  c.desc "Edit entry with #{Doing::Util.default_editor}"
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc 'Archive previous @meanwhile entry'
  c.switch %i[a archive], negatable: false, default_value: false

  c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
  c.arg_name 'DATE_STRING'
  c.flag %i[b back]

  c.desc 'Note'
  c.arg_name 'TEXT'
  c.flag %i[n note]

  c.action do |_global_options, options, args|
    if options[:back]
      date = wwid.chronify(options[:back], guess: :begin)

      raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
    else
      date = Time.now
    end

    if options[:section]
      section = wwid.guess_section(options[:section]) || options[:section].cap_first
    else
      section = settings['current_section']
    end
    input = ''

    if options[:e]
      raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?

      input += args.join(' ') unless args.empty?
      input = wwid.fork_editor(input).strip
    elsif !args.empty?
      input = args.join(' ')
    elsif $stdin.stat.size.positive?
      input = $stdin.read
    end

    if input && !input.empty?
      input, note = wwid.format_input(input)
    else
      input = nil
      note = []
    end

    if options[:n]
      note.push(options[:n])
    elsif note.empty?
      note = nil
    end

    wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:a], note: note })
    wwid.write(wwid.doing_file)
  end
end

desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
long_desc %(
  Templates are printed to STDOUT for piping to a file.
  Save them and use them in the configuration file under export_templates.
)
arg_name 'TYPE', must_match: Doing::Plugins.template_regex
command :template do |c|
  c.example 'doing template haml > ~/styles/my_doing.haml', desc: 'Output the haml template and save it to a file'

  c.desc 'List all available templates'
  c.switch %i[l list], negatable: false

  c.desc 'List in single column for completion'
  c.switch %i[c]

  c.action do |_global_options, options, args|
    if options[:list] || options[:c]
      if options[:c]
        $stdout.print Doing::Plugins.plugin_templates.join("\n")
      else
        $stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
      end
      return
    end

    if args.empty?
      type = wwid.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
    else
      type = args[0]
    end

    raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type

    $stdout.puts Doing::Plugins.template_for_trigger(type)

    # case args[0]
    # when /html|haml/i
    #   $stdout.puts wwid.haml_template
    # when /css/i
    #   $stdout.puts wwid.css_template
    # when /markdown|md|erb/i
    #   $stdout.puts wwid.markdown_template
    # else
    #   exit_now! 'Invalid type specified, must be HAML or CSS'
    # end
  end
end

desc 'Display an interactive menu to perform operations'
long_desc 'List all entries and select with typeahead fuzzy matching.

Multiple selections are allowed, hit tab to add the highlighted entry to the
selection, and use ctrl-a to select all visible items. Return processes the
selected entries.'
command :select do |c|
  c.desc 'Select from a specific section'
  c.arg_name 'SECTION'
  c.flag %i[s section]

  c.desc 'Tag selected entries'
  c.arg_name 'TAG'
  c.flag %i[t tag]

  c.desc 'Reverse -c, -f, --flag, and -t (remove instead of adding)'
  c.switch %i[r remove], negatable: false

  # c.desc 'Add @done to selected item(s), using start time of next item as the finish time'
  # c.switch %i[a auto], negatable: false, default_value: false

  c.desc 'Archive selected items'
  c.switch %i[a archive], negatable: false, default_value: false

  c.desc 'Move selected items to section'
  c.arg_name 'SECTION'
  c.flag %i[m move]

  c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
  c.arg_name 'QUERY'
  c.flag %i[q query search]

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Select items that *don\'t* match search/tag filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
  c.switch %i[menu], negatable: true, default_value: true

  c.desc 'Cancel selected items (add @done without timestamp)'
  c.switch %i[c cancel], negatable: false, default_value: false

  c.desc 'Delete selected items'
  c.switch %i[d delete], negatable: false, default_value: false

  c.desc 'Edit selected item(s)'
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc 'Add @done with current time to selected item(s)'
  c.switch %i[f finish], negatable: false, default_value: false

  c.desc 'Add flag to selected item(s)'
  c.switch %i[flag], negatable: false, default_value: false

  c.desc 'Perform action without confirmation.'
  c.switch %i[force], negatable: false, default_value: false

  c.desc 'Save selected entries to file using --output format'
  c.arg_name 'FILE'
  c.flag %i[save_to]

  c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]

  c.desc "Copy selection as a new entry with current time and no @done tag. Only works with single selections. Can be combined with --editor."
  c.switch %i[again resume], negatable: false, default_value: false

  c.action do |_global_options, options, args|
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]

    options[:case] = options[:case].normalize_case

    wwid.interactive(options)
  end
end

desc 'Add an item to the Later section'
arg_name 'ENTRY'
command :later do |c|
  c.example 'doing later "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Later section'
  c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note'

  c.desc "Edit entry with #{Doing::Util.default_editor}"
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
  c.arg_name 'DATE_STRING'
  c.flag %i[b back]

  c.desc 'Note'
  c.arg_name 'TEXT'
  c.flag %i[n note]

  c.action do |_global_options, options, args|
    if options[:back]
      date = wwid.chronify(options[:back], guess: :begin)
      raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
    else
      date = Time.now
    end

    if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
      raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?

      input = args.empty? ? '' : args.join(' ')
      input = wwid.fork_editor(input).strip
      raise EmptyInput, 'No content' unless input && !input.empty?

      title, note = wwid.format_input(input)
      note.push(options[:n]) if options[:n]
      wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
      wwid.write(wwid.doing_file)
    elsif !args.empty?
      title, note = wwid.format_input(args.join(' '))
      note.push(options[:n]) if options[:n]
      wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
      wwid.write(wwid.doing_file)
    elsif $stdin.stat.size.positive?
      title, note = wwid.format_input($stdin.read)
      note.push(options[:n]) if options[:n]
      wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
      wwid.write(wwid.doing_file)
    else
      raise EmptyInput, 'You must provide content when creating a new entry'
    end
  end
end

desc 'Add a completed item with @done(date). No argument finishes last entry.'
arg_name 'ENTRY'
command %i[done did] do |c|
  c.example 'doing done', desc: 'Tag the last entry @done'
  c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
  c.example 'doing done --back 30m This took me half an hour', desc: 'Add an entry with a start date 30 minutes ago and a @done date of right now'
  c.example 'doing done --at 3pm --took 1h Started and finished this afternoon', desc: 'Add an entry with a @done date of 3pm and a start date of 2pm (3pm - 1h)'

  c.desc 'Remove @done tag'
  c.switch %i[r remove], negatable: false, default_value: false

  c.desc 'Include date'
  c.switch [:date], negatable: true, default_value: true

  c.desc 'Immediately archive the entry'
  c.switch %i[a archive], negatable: false, default_value: false

  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
  If used, ignores --back. Used with --took, backdates start date)
  c.arg_name 'DATE_STRING'
  c.flag [:at]

  c.desc 'Backdate start date by interval [4pm|20m|2h|yesterday noon]'
  c.arg_name 'DATE_STRING'
  c.flag %i[b back]

  c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
  If used without the --back option, the start date will be moved back to allow
  the completion date to be the current time.)
  c.arg_name 'INTERVAL'
  c.flag %i[t took]

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section]

  c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc 'Include a note'
  c.arg_name 'TEXT'
  c.flag %i[n note]

  c.desc 'Finish last entry not already marked @done'
  c.switch %i[u unfinished], negatable: false, default_value: false

  # c.desc "Edit entry with specified app"
  # c.arg_name 'editor_app'
  # # c.flag [:a, :app]

  c.action do |_global_options, options, args|
    took = 0
    donedate = nil

    if options[:took]
      took = wwid.chronify_qty(options[:took])
      raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
    end

    if options[:back]
      date = wwid.chronify(options[:back], guess: :begin)
      raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
    else
      date = options[:took] ? Time.now - took : Time.now
    end

    if options[:at]
      finish_date = wwid.chronify(options[:at], guess: :begin)
      raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?

      date = options[:took] ? finish_date - took : finish_date
    elsif options[:took]
      finish_date = date + took
    elsif options[:back]
      finish_date = date
    else
      finish_date = Time.now
    end

    if options[:date]
      donedate = finish_date.strftime('%F %R')
    end

    if options[:section]
      section = wwid.guess_section(options[:section]) || options[:section].cap_first
    else
      section = settings['current_section']
    end

    note = Doing::Note.new
    note.add(options[:note]) if options[:note]

    if options[:editor]
      raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
      is_new = false

      if args.empty?
        last_entry = wwid.filter_items([], opt: {unfinished: options[:unfinished], section: section, count: 1, age: 'new'}).max_by { |item| item.date }

        unless last_entry
          Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
          raise NoResults, 'No results'
        end

        old_entry = last_entry.dup
        last_entry.note.add(note)
        input = [last_entry.title, last_entry.note.to_s].join("\n")
      else
        is_new = true
        input = [args.join(' '), note.to_s].join("\n")
      end

      input = wwid.fork_editor(input).strip
      raise EmptyInput, 'No content' unless input && !input.empty?

      title, note = wwid.format_input(input)
      new_entry = Doing::Item.new(date, title, section, note)
      if new_entry.should_finish?
        if new_entry.should_time?
          new_entry.tag('done', value: donedate)
        else
          new_entry.tag('done')
        end
      end

      if (is_new)
        wwid.content[section][:items].push(new_entry)
      else
        wwid.update_item(old_entry, new_entry)
      end

      if options[:a]
        wwid.move_item(new_entry, 'Archive', label: true)
      end

      wwid.write(wwid.doing_file)
    elsif args.empty? && $stdin.stat.size.zero?
      if options[:r]
        wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
      else
        note = options[:note] ? Doing::Note.new(options[:note]) : nil
        opt = {
          archive: options[:a],
          back: finish_date,
          count: 1,
          date: options[:date],
          note: note,
          section: section,
          tags: ['done'],
          took: took == 0 ? nil : took,
          unfinished: options[:unfinished]
        }
        wwid.tag_last(opt)
      end
    elsif !args.empty?
      note = Doing::Note.new(options[:note])
      title, new_note = wwid.format_input([args.join(' '), note.to_s].join("\n"))
      title.chomp!
      section = 'Archive' if options[:a]
      new_entry = Doing::Item.new(date, title, section, new_note)
      if new_entry.should_finish?
        if new_entry.should_time?
          new_entry.tag('done', value: donedate)
        else
          new_entry.tag('done')
        end
      end
      wwid.content[section][:items].push(new_entry)
      wwid.write(wwid.doing_file)
      Doing.logger.info('Entry Added:', new_entry.title)
    elsif $stdin.stat.size.positive?
      title, note = wwid.format_input($stdin.read)
      note.add(options[:note]) if options[:note]
      section = options[:a] ? 'Archive' : section
      new_entry = Doing::Item.new(date, title, section, note)

      if new_entry.should_finish?
        if new_entry.should_time?
          new_entry.tag('done', value: donedate)
        else
          new_entry.tag('done')
        end
      end

      wwid.content[section][:items].push(new_entry)
      wwid.write(wwid.doing_file)
      Doing.logger.info('Entry Added:', new_entry.title)
    else
      raise EmptyInput, 'You must provide content when creating a new entry'
    end
  end
end

desc 'End last X entries with no time tracked'
long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`.'
arg_name 'COUNT'
command :cancel do |c|
  c.example 'doing cancel', desc: 'Cancel the last entry'
  c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'

  c.desc 'Archive entries'
  c.switch %i[a archive], negatable: false, default_value: false

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section]

  c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2)'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Finish items that *don\'t* match search/tag filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Cancel last entry (or entries) not already marked @done'
  c.switch %i[u unfinished], negatable: false, default_value: false

  c.desc 'Select item(s) to cancel from a menu of matching entries'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    if options[:section]
      section = wwid.guess_section(options[:section]) || options[:section].cap_first
    else
      section = settings['current_section']
    end

    if options[:tag].nil?
      tags = []
    else
      tags = options[:tag].to_tags
    end

    raise InvalidArgument, 'Only one argument allowed' if args.length > 1

    raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/

    if options[:interactive]
      count = 0
    else
      count = args[0] ? args[0].to_i : 1
    end

    search = nil

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
    end

    opts = {
      archive: options[:a],
      case: options[:case].normalize_case,
      count: count,
      date: false,
      fuzzy: options[:fuzzy],
      interactive: options[:interactive],
      not: options[:not],
      search: search,
      section: section,
      sequential: false,
      tag: tags,
      tag_bool: options[:bool].normalize_bool,
      tags: ['done'],
      unfinished: options[:unfinished]
    }

    wwid.tag_last(opts)
  end
end

desc 'Mark last X entries as @done'
long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
arg_name 'COUNT'
command :finish do |c|
  c.example 'doing finish', desc: 'Mark the last entry @done'
  c.example 'doing finish --auto --section Later 10', desc: 'Add @done to any unfinished entries in the last 10 in Later, setting the finish time based on the start time of the task after it'
  c.example 'doing finish --search "a specific entry" --at "yesterday 3pm"', desc: 'Search for an entry containing string and set its @done time to yesterday at 3pm'

  c.desc 'Include date'
  c.switch [:date], negatable: true, default_value: true

  c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
  c.arg_name 'DATE_STRING'
  c.flag %i[b back]

  c.desc 'Set the completed date to the start date plus XX[hmd]'
  c.arg_name 'INTERVAL'
  c.flag %i[t took]

  c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
  c.arg_name 'DATE_STRING'
  c.flag [:at]

  c.desc 'Finish the last X entries containing TAG.
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Finish the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Finish items that *don\'t* match search/tag filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Remove done tag'
  c.switch %i[r remove], negatable: false, default_value: false

  c.desc 'Finish last entry (or entries) not already marked @done'
  c.switch %i[u unfinished], negatable: false, default_value: false

  c.desc %(Auto-generate finish dates from next entry's start time.
  Automatically generate completion dates 1 minute before next item (in any section) began.
  --auto overrides the --date and --back parameters.)
  c.switch [:auto], negatable: false, default_value: false

  c.desc 'Archive entries'
  c.switch %i[a archive], negatable: false, default_value: false

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section]

  c.desc 'Select item(s) to finish from a menu of matching entries'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    unless options[:auto]
      if options[:took]
        took = wwid.chronify_qty(options[:took])
        raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
      end

      raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]

      raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]

      if options[:at]
        finish_date = wwid.chronify(options[:at], guess: :begin)
        raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?

        date = options[:took] ? finish_date - took : finish_date
      elsif options[:back]
        date = wwid.chronify(options[:back])

        raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
      elsif options[:took]
        date = wwid.chronify_qty(options[:took])
      else
        date = Time.now
      end
    end

    if options[:tag].nil?
      tags = []
    else
      tags = options[:tag].to_tags
    end

    raise InvalidArgument, 'Only one argument allowed' if args.length > 1

    raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/

    if options[:interactive]
      count = 0
    else
      count = args[0] ? args[0].to_i : 1
    end

    search = nil

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
    end

    opts = {
      archive: options[:archive],
      back: date,
      case: options[:case].normalize_case,
      count: count,
      date: options[:date],
      fuzzy: options[:fuzzy],
      interactive: options[:interactive],
      not: options[:not],
      remove: options[:remove],
      search: search,
      section: options[:section],
      sequential: options[:auto],
      tag: tags,
      tag_bool: options[:bool].normalize_bool,
      tags: ['done'],
      unfinished: options[:unfinished]
    }

    wwid.tag_last(opts)
  end
end

desc 'Repeat last entry as new entry'
command %i[again resume] do |c|
  c.desc 'Get last entry from a specific section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Add new entry to section (default: same section as repeated entry)'
  c.arg_name 'SECTION_NAME'
  c.flag [:in]

  c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma.'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Repeat last entry matching search. Surround with
  slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Resume items that *don\'t* match search/tag filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Boolean used to combine multiple tags'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc 'Note'
  c.arg_name 'TEXT'
  c.flag %i[n note]

  c.desc 'Select item to resume from a menu of matching entries'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |_global_options, options, _args|
    options[:fuzzy] = false
    tags = options[:tag].nil? ? [] : options[:tag].to_tags

    options[:case] = options[:case].normalize_case

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
      options[:search] = search
    end

    opts = options.dup

    opts[:tag] = tags
    opts[:tag_bool] = options[:bool].normalize_bool
    opts[:interactive] = options[:interactive]

    wwid.repeat_last(opts)
  end
end

desc 'Add tag(s) to last entry'
long_desc 'Add (or remove) tags from the last entry, or from multiple entries
          (with `--count`), entries matching a search (with `--search`), or entries
          containing another tag (with `--tag`).

          When removing tags with `-r`, wildcards are allowed (`*` to match
          multiple characters, `?` to match a single character). With `--regex`,
          regular expressions will be interpreted instead of wildcards.

          For all tag removals the match is case insensitive by default, but if
          the tag search string contains any uppercase letters, the match will
          become case sensitive automatically.

          Tag name arguments do not need to be prefixed with @.'
arg_name 'TAG', :multiple
command :tag do |c|
  c.example 'doing tag mytag', desc: 'Add @mytag to the last entry created'
  c.example 'doing tag --remove mytag', desc: 'Remove @mytag from the last entry created'
  c.example 'doing tag --rename "other*" --count 10 newtag', desc: 'Rename tags beginning with "other" (wildcard) to @newtag on the last 10 entries'
  c.example 'doing tag --search "developing" coding', desc: 'Add @coding to the last entry containing string "developing" (fuzzy matching)'
  c.example 'doing tag --interactive --tag project1 coding', desc: 'Create an interactive menu from entries tagged @project1, selection(s) will be tagged with @coding'

  c.desc 'Section'
  c.arg_name 'SECTION_NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'How many recent entries to tag (0 for all)'
  c.arg_name 'COUNT'
  c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer

  c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
  c.arg_name 'ORIG_TAG'
  c.flag %i[rename]

  c.desc 'Don\'t ask permission to tag all entries when count is 0'
  c.switch %i[force], negatable: false, default_value: false

  c.desc 'Include current date/time with tag'
  c.switch %i[d date], negatable: false, default_value: false

  c.desc 'Remove given tag(s)'
  c.switch %i[r remove], negatable: false, default_value: false

  c.desc 'Interpret tag string as regular expression (with --remove)'
  c.switch %i[regex], negatable: false, default_value: false

  c.desc 'Tag last entry (or entries) not marked @done'
  c.switch %i[u unfinished], negatable: false, default_value: false

  c.desc 'Autotag entries based on autotag configuration in ~/.doingrc'
  c.switch %i[a autotag], negatable: false, default_value: false

  c.desc 'Tag the last X entries containing TAG.
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Tag items that *don\'t* match search/tag filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Select item(s) to tag from a menu of matching entries'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]

    raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]

    section = 'All'

    if options[:section]
      section = wwid.guess_section(options[:section]) || options[:section].cap_first
    end


    if options[:tag].nil?
      search_tags = []
    else
      search_tags = options[:tag].to_tags
    end

    if options[:autotag]
      tags = []
    else
      tags = if args.join('') =~ /,/
               args.join('').split(/,/)
             else
               args.join(' ').split(' ') # in case tags are quoted as one arg
             end

      tags.map! { |tag| tag.sub(/^@/, '').strip }
    end

    if options[:interactive]
      count = 0
      options[:force] = true
    else
      count = options[:count].to_i
    end

    options[:case] ||= :smart
    options[:case] = options[:case].normalize_case

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
      options[:search] = search
    end

    if count.zero? && !options[:force]
      if options[:search]
        section_q = ' matching your search terms'
      elsif options[:tag]
        section_q = ' matching your tag search'
      elsif section == 'All'
        section_q = ''
      else
        section_q = " in section #{section}"
      end


      question = if options[:a]
                   "Are you sure you want to autotag all records#{section_q}"
                 elsif options[:r]
                   "Are you sure you want to remove #{tags.join(' and ')} from all records#{section_q}"
                 else
                   "Are you sure you want to add #{tags.join(' and ')} to all records#{section_q}"
                 end

      res = wwid.yn(question, default_response: false)

      raise UserCancelled unless res
    end

    options[:count] = count
    options[:section] = section
    options[:tag] = search_tags
    options[:tags] = tags
    options[:tag_bool] = options[:bool].normalize_bool

    wwid.tag_last(options)
  end
end

desc 'Mark last entry as flagged'
command [:mark, :flag] do |c|
  c.example 'doing flag', desc: 'Add @flagged to the last entry created'
  c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
  c.example 'doing flag --interactive --search "/(develop|cod)ing/"', desc: 'Find entries matching regular expression and create a menu allowing multiple selections, selected items will be @flagged'

  c.desc 'Section'
  c.arg_name 'SECTION_NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'How many recent entries to tag (0 for all)'
  c.arg_name 'COUNT'
  c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer

  c.desc 'Don\'t ask permission to flag all entries when count is 0'
  c.switch %i[force], negatable: false, default_value: false

  c.desc 'Include current date/time with tag'
  c.switch %i[d date], negatable: false, default_value: false

  c.desc 'Remove flag'
  c.switch %i[r remove], negatable: false, default_value: false

  c.desc 'Flag last entry (or entries) not marked @done'
  c.switch %i[u unfinished], negatable: false, default_value: false

  c.desc 'Flag the last entry containing TAG.
  Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Flag the last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Flag items that *don\'t* match search/tag/date filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Select item(s) to flag from a menu of matching entries'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |_global_options, options, _args|
    options[:fuzzy] = false
    mark = settings['marker_tag'] || 'flagged'

    raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]

    section = 'All'

    if options[:section]
      section = wwid.guess_section(options[:section]) || options[:section].cap_first
    end

    if options[:tag].nil?
      search_tags = []
    else
      search_tags = options[:tag].to_tags
    end

    if options[:interactive]
      count = 0
      options[:force] = true
    else
      count = options[:count].to_i
    end

    options[:case] = options[:case].normalize_case

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
      options[:search] = search
    end

    if count.zero? && !options[:force]
      if options[:search]
        section_q = ' matching your search terms'
      elsif options[:tag]
        section_q = ' matching your tag search'
      elsif section == 'All'
        section_q = ''
      else
        section_q = " in section #{section}"
      end


      question = if options[:remove]
                   "Are you sure you want to unflag all entries#{section_q}"
                 else
                   "Are you sure you want to flag all records#{section_q}"
                 end

      res = wwid.yn(question, default_response: false)

      exit_now! 'Cancelled' unless res
    end

    options[:count] = count
    options[:section] = section
    options[:tag] = search_tags
    options[:tags] = [mark]
    options[:tag_bool] = options[:bool].normalize_bool

    wwid.tag_last(options)
  end
end

desc 'List all entries'
long_desc %(
  The argument can be a section name, @tag(s) or both.
  "pick" or "choose" as an argument will offer a section menu.
)
arg_name '[SECTION|@TAGS]'
command :show do |c|
  c.example 'doing show Currently', desc: 'Show entries in the Currently section'
  c.example 'doing show @project1', desc: 'Show entries tagged @project1'
  c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
  c.example 'doing show @oracle @writing --bool and', desc: 'Show entries tagged both @oracle and @writing'
  c.example 'doing show Currently @devo --bool not', desc: 'Show entries in Currently NOT tagged @devo'
  c.example 'doing show Ideas @doing --from "mon to fri"', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.'
  c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'

  c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Tag boolean (AND,OR,NOT)'
  c.arg_name 'BOOLEAN'
  c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'

  c.desc 'Max count to show'
  c.arg_name 'MAX'
  c.flag %i[c count], default_value: 0

  c.desc 'Age (oldest|newest)'
  c.arg_name 'AGE'
  c.flag %i[a age], default_value: 'newest'

  c.desc 'View entries older than date'
  c.arg_name 'DATE_STRING'
  c.flag [:before]

  c.desc 'View entries newer than date'
  c.arg_name 'DATE_STRING'
  c.flag [:after]

  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Show items that *don\'t* match search/tag/date filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Sort order (asc/desc)'
  c.arg_name 'ORDER'
  c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'

  c.desc %(
    Date range to show, or a single day to filter date on.
    Date range argument should be quoted. Date specifications can be natural language.
    To specify a range, use "to" or "through": `doing show --from "monday to friday"`
  )
  c.arg_name 'DATE_OR_RANGE'
  c.flag %i[f from]

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show intervals with totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Sort tags by (name|time)'
  default = 'time'
  default = settings['tag_sort'] || 'name'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default

  c.desc 'Tag sort direction (asc|desc)'
  c.arg_name 'DIRECTION'
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'

  c.desc 'Only show items with recorded time intervals'
  c.switch [:only_timed], default_value: false, negatable: false

  c.desc 'Select from a menu of matching entries to perform additional operations'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]
  c.action do |global_options, options, args|
    options[:fuzzy] = false
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    tag_filter = false
    tags = []
    if args.length.positive?
      case args[0]
      when /^all$/i
        section = 'All'
        args.shift
      when /^(choose|pick)$/i
        section = wwid.choose_section
        args.shift
      when /^@/
        section = 'All'
      else
        begin
          section = wwid.guess_section(args[0])
        rescue WrongCommand => exception
          cmd = commands[:view]
          action = cmd.send(:get_action, nil)
          return action.call(global_options, options, args)
        end

        raise InvalidSection, "No such section: #{args[0]}" unless section

        args.shift
      end
      if args.length.positive?
        args.each do |arg|
          arg.split(/,/).each do |tag|
                        tags.push(tag.strip.sub(/^@/, ''))
                      end
        end
      end
    else
      section = settings['current_section']
    end

    tags.concat(options[:tag].to_tags) if options[:tag]

    unless tags.empty?
      tag_filter = {
        'tags' => tags,
        'bool' => options[:bool].normalize_bool
      }
    end

    if options[:from]

      date_string = options[:from]
      if date_string =~ / (to|through|thru|(un)?til|-+) /
        dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
        start = wwid.chronify(dates[0], guess: :begin)
        finish = wwid.chronify(dates[2], guess: :end)
      else
        start = wwid.chronify(date_string, guess: :begin)
        finish = false
      end
      raise InvalidTimeExpression, 'Unrecognized date string' unless start
      dates = [start, finish]
    end

    options[:times] = true if options[:totals]

    template = settings['templates']['default'].deep_merge({
        'wrap_width' => settings['wrap_width'] || 0,
        'date_format' => settings['default_date_format'],
        'order' => settings['order'] || 'asc',
        'tags_color' => settings['tags_color']
      })

    options[:case] = options[:case].normalize_case

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
      options[:search] = search
    end

    opt = options.dup

    opt[:sort_tags] = options[:tag_sort] =~ /^n/i
    opt[:count] = options[:count].to_i
    opt[:date_filter] = dates
    opt[:highlight] = true
    opt[:order] = options[:sort].normalize_order
    opt[:section] = section
    opt[:tag] = nil
    opt[:tag_filter] = tag_filter
    opt[:tag_order] = options[:tag_order].normalize_order
    opt[:tags_color] = template['tags_color']

    Doing::Pager.page wwid.list_section(opt)
  end
end

desc 'Search for entries'
long_desc <<~'EODESC'
  Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.

  To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
EODESC

arg_name 'SEARCH_PATTERN'
command %i[grep search] do |c|
  c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
  c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
  c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
  c.example 'doing search --before 12/21 "doing wiki"', desc: 'Find entries containing "doing wiki" with entry dates before 12/21 of the current year'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Constrain search to entries older than date'
  c.arg_name 'DATE_STRING'
  c.flag [:before]

  c.desc 'Constrain search to entries newer than date'
  c.arg_name 'DATE_STRING'
  c.flag [:after]

  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show intervals with totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Sort tags by (name|time)'
  default = 'time'
  default = settings['tag_sort'] || 'name'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default

  c.desc 'Only show items with recorded time intervals'
  c.switch [:only_timed], default_value: false, negatable: false

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Show items that *don\'t* match search string'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Display an interactive menu of results to perform further operations'
  c.switch %i[i interactive], default_value: false, negatable: false

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    template = settings['templates']['default'].deep_merge(settings)
    tags_color = template.key?('tags_color') ? template['tags_color'] : nil

    section = wwid.guess_section(options[:section]) if options[:section]

    options[:case] = options[:case].normalize_case

    search = args.join(' ')
    search.sub!(/^'?/, "'") if options[:exact]

    options[:times] = true if options[:totals]
    options[:sort_tags] = options[:tag_sort] =~ /^n/i
    options[:highlight] = true
    options[:search] = search
    options[:section] = section
    options[:tags_color] = tags_color

    Doing::Pager.page wwid.list_section(options)
  end
end

desc 'List recent entries'
default_value 10
arg_name 'COUNT'
command :recent do |c|
  c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections'
  c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections'
  c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section'
  c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show intervals with totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Sort tags by (name|time)'
  default = 'time'
  default = settings['tag_sort'] || 'name'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default

  c.desc 'Select from a menu of matching entries to perform additional operations'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |global_options, options, args|
    section = wwid.guess_section(options[:s]) || options[:s].cap_first

    unless global_options[:version]
      if settings['templates']['recent'].key?('count')
        config_count = settings['templates']['recent']['count'].to_i
      else
        config_count = 10
      end

      if options[:interactive]
        count = 0
      else
        count = args.empty? ? config_count : args[0].to_i
      end

      options[:t] = true if options[:totals]
      options[:sort_tags] = options[:tag_sort] =~ /^n/i

      template = settings['templates']['recent'].deep_merge(settings['templates']['default'])
      tags_color = template.key?('tags_color') ? template['tags_color'] : nil

      opts = {
        sort_tags: options[:sort_tags],
        tags_color: tags_color,
        times: options[:t],
        totals: options[:totals],
        interactive: options[:interactive]
      }

      Doing::Pager::page wwid.recent(count, section.cap_first, opts)

    end
  end
end

desc 'List entries from today'
command :today do |c|
  c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
  c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
  c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
  c.example 'doing today --output json', desc: 'Output entries from today in JSON format'

  c.desc 'Specify a section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show time totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Sort tags by (name|time)'
  default = 'time'
  default = settings['tag_sort'] || 'name'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default

  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]

  c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
  c.arg_name 'TIME_STRING'
  c.flag [:before]

  c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)'
  c.arg_name 'TIME_STRING'
  c.flag [:after]

  c.action do |_global_options, options, _args|
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    options[:t] = true if options[:totals]
    options[:sort_tags] = options[:tag_sort] =~ /^n/i
    opt = {
      after: options[:after],
      before: options[:before],
      section: options[:section],
      sort_tags: options[:sort_tags],
      totals: options[:totals]
    }
    Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
  end
end

desc 'List entries for a date'
long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
it will create a range.)
arg_name 'DATE_STRING'
command :on do |c|
  c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
  c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
  c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show time totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Sort tags by (name|time)'
  default = 'time'
  default = settings['tag_sort'] || 'name'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default

  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]

  c.action do |_global_options, options, args|
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    raise MissingArgument, 'Missing date argument' if args.empty?

    date_string = args.join(' ')

    if date_string =~ / (to|through|thru) /
      dates = date_string.split(/ (to|through|thru) /)
      start = wwid.chronify(dates[0], guess: :begin)
      finish = wwid.chronify(dates[2], guess: :end)
    else
      start = wwid.chronify(date_string, guess: :begin)
      finish = false
    end

    raise InvalidTimeExpression, 'Unrecognized date string' unless start

    message = "Date interpreted as #{start}"
    message += " to #{finish}" if finish
    Doing.logger.debug(message)

    options[:t] = true if options[:totals]
    options[:sort_tags] = options[:tag_sort] =~ /^n/i

    Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
                        { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
  end
end

desc 'List entries since a date'
long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday,"
and "2d" would be interpreted as "two days ago.")
arg_name 'DATE_STRING'
command :since do |c|
  c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
  c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show time totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Sort tags by (name|time)'
  default = 'time'
  default = settings['tag_sort'] || 'name'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default

  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]

  c.action do |_global_options, options, args|
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    raise MissingArgument, 'Missing date argument' if args.empty?

    date_string = args.join(' ')

    date_string.sub!(/(day) (\d)/, '\1 at \2')
    date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')

    start = wwid.chronify(date_string, guess: :begin)
    finish = Time.now

    raise InvalidTimeExpression, 'Unrecognized date string' unless start

    Doing.logger.debug("Date interpreted as #{start} through the current time")

    options[:t] = true if options[:totals]
    options[:sort_tags] = options[:tag_sort] =~ /^n/i

    Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
                        { totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
  end
end

desc 'List entries from yesterday'
command :yesterday do |c|
  c.example 'doing yesterday', desc: 'List all entries from the previous day'
  c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
  c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers'

  c.desc 'Specify a section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show time totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Sort tags by (name|time)'
  default = settings['tag_sort'] || 'name'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default

  c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
  c.arg_name 'TIME_STRING'
  c.flag [:before]

  c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)'
  c.arg_name 'TIME_STRING'
  c.flag [:after]

  c.desc 'Tag sort direction (asc|desc)'
  c.arg_name 'DIRECTION'
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'

  c.action do |_global_options, options, _args|
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    options[:sort_tags] = options[:tag_sort] =~ /^n/i

    opt = {
      after: options[:after],
      before: options[:before],
      sort_tags: options[:sort_tags],
      tag_order: options[:tag_order].normalize_order,
      totals: options[:totals],
      order: settings.dig('templates', 'today', 'order')
    }
    Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
  end
end

desc 'Show the last entry, optionally edit'
command :last do |c|
  c.example 'doing last', desc: 'Show the most recent entry in all sections'
  c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
  c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
  c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
  c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
  c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
  c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'

  c.desc 'Specify a section'
  c.arg_name 'NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc "Edit entry with #{Doing::Util.default_editor}"
  c.switch %i[e editor], negatable: false, default_value: false

  c.desc 'Tag filter, combine multiple tags with a comma.'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Tag boolean'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Show items that *don\'t* match search string or tag filter'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.action do |global_options, options, _args|
    options[:fuzzy] = false
    raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]

    if options[:tag].nil?
      tags = []
    else
      tags = options[:tag].to_tags
      options[:bool] = case options[:bool]
                       when /(any|or)/i
                        :or
                       when /(not|none)/i
                        :not
                       else
                        :and
                       end

    end

    options[:case] = options[:case].normalize_case

    search = nil

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
    end

    if options[:editor]
      wwid.edit_last(section: options[:s], options: { search: search, fuzzy: options[:fuzzy], case: options[:case], tag: tags, tag_bool: options[:bool], not: options[:not] })
    else
      Doing::Pager::page wwid.last(times: true, section: options[:s],
                     options: { search: search, fuzzy: options[:fuzzy], case: options[:case], negate: options[:not], tag: tags, tag_bool: options[:bool] }).strip
    end
  end
end

desc 'List sections'
command :sections do |c|
  c.desc 'List in single column'
  c.switch %i[c column], negatable: false, default_value: false

  c.action do |_global_options, options, _args|
    joiner = options[:c] ? "\n" : "\t"
    print wwid.sections.join(joiner)
  end
end

desc 'Select a section to display from a menu'
command :choose do |c|
  c.action do |_global_options, _options, _args|
    section = wwid.choose_section

    Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
  end
end

desc 'Add a new section to the "doing" file'
arg_name 'SECTION_NAME'
command :add_section do |c|
  c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'

  c.action do |_global_options, _options, args|
    raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])

    wwid.add_section(args.join(' ').cap_first)
    wwid.write(wwid.doing_file)
  end
end

desc 'List available color variables for configuration templates and views'
command :colors do |c|
  c.action do |_global_options, _options, _args|
    bgs = []
    fgs = []
    colors::attributes.each do |color|
      if color.to_s =~ /bg/
        bgs.push("#{colors.send(color, "    ")}#{colors.default} <-- #{color.to_s}")
      else
        fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
      end
    end
    out = []
    out << fgs.join("\n")
    out << bgs.join("\n")
    Doing::Pager.page out.join("\n")
  end
end

desc 'List installed plugins'
long_desc %(Lists available plugins, including user-installed plugins.

Export plugins are available with the `--output` flag on commands that support it.

Import plugins are available using `doing import --type PLUGIN`.
)
command :plugins do |c|
  c.example 'doing plugins', desc: 'List all plugins'
  c.example 'doing plugins -t import', desc: 'List all import plugins'

  c.desc 'List plugins of type (import, export)'
  c.arg_name 'TYPE'
  c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'

  c.desc 'List in single column for completion'
  c.switch %i[c column], negatable: false, default_value: false

  c.action do |_global_options, options, _args|
    Doing::Plugins.list_plugins(options)
  end
end

desc 'Generate shell completion scripts'
command :completion do |c|
  c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
  c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
  c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
  c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'

  c.desc 'Shell to generate for (bash, zsh, fish)'
  c.arg_name 'SHELL'
  c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'

  c.desc 'File to write output to'
  c.arg_name 'PATH'
  c.flag %i[f file], default_value: 'stdout'

  c.action do |_global_options, options, _args|
    script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')

    case options[:type]
    when /^b/
      result = `ruby #{File.join(script_dir, 'generate_bash_completions.rb')}`
    when /^z/
      result = `ruby #{File.join(script_dir, 'generate_zsh_completions.rb')}`
    when /^f/
      result = `ruby #{File.join(script_dir, 'generate_fish_completions.rb')}`
    end

    if options[:file] =~ /^stdout$/i
      $stdout.puts result
    else
      File.open(File.expand_path(options[:file]), 'w') do |f|
        f.puts result
      end
      Doing.logger.warn('File written:', "#{options[:type]} completions written to #{options[:file]}")
    end
  end
end

desc 'Display a user-created view'
long_desc 'Command line options override view configuration'
arg_name 'VIEW_NAME'
command :view do |c|
  c.example 'doing view color', desc: 'Display entries according to config for view "color"'
  c.example 'doing view color --section Archive --count 10', desc: 'Display view "color", overriding some configured settings'

  c.desc 'Section'
  c.arg_name 'NAME'
  c.flag %i[s section]

  c.desc 'Count to display'
  c.arg_name 'COUNT'
  c.flag %i[c count], must_match: /^\d+$/, type: Integer

  c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
  c.arg_name 'FORMAT'
  c.flag %i[o output]

  c.desc 'Show time intervals on @done tasks'
  c.switch %i[t times], default_value: true, negatable: true

  c.desc 'Show intervals with totals at the end of output'
  c.switch [:totals], default_value: false, negatable: false

  c.desc 'Include colors in output'
  c.switch [:color], default_value: true, negatable: true

  c.desc 'Tag filter, combine multiple tags with a comma.'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Tag boolean (AND,OR,NOT)'
  c.arg_name 'BOOLEAN'
  c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'

  c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Show items that *don\'t* match search string'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Sort tags by (name|time)'
  c.arg_name 'KEY'
  c.flag [:tag_sort], must_match: /^(?:name|time)$/i

  c.desc 'Tag sort direction (asc|desc)'
  c.arg_name 'DIRECTION'
  c.flag [:tag_order], must_match: REGEX_SORT_ORDER

  c.desc 'View entries older than date'
  c.arg_name 'DATE_STRING'
  c.flag [:before]

  c.desc 'View entries newer than date'
  c.arg_name 'DATE_STRING'
  c.flag [:after]

  c.desc 'Only show items with recorded time intervals (override view settings)'
  c.switch [:only_timed], default_value: false, negatable: false

  c.desc 'Select from a menu of matching entries to perform additional operations'
  c.switch %i[i interactive], negatable: false, default_value: false

  c.action do |global_options, options, args|
    options[:fuzzy] = false
    raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)

    raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]

    title = if args.empty?
              wwid.choose_view
            else
              begin
                wwid.guess_view(args[0])
              rescue WrongCommand => exception
                cmd = commands[:show]
                options[:sort] = 'asc'
                action = cmd.send(:get_action, nil)
                return action.call(global_options, options, args)
              end
            end

    if options[:section]
      section = wwid.guess_section(options[:section]) || options[:section].cap_first
    else
      section = settings['current_section']
    end

    view = wwid.get_view(title)
    if view
      page_title = view.key?('title') ? view['title'] : title.cap_first
      only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
                     true
                   else
                     false
                   end

      template = view.key?('template') ? view['template'] : nil
      date_format = view.key?('date_format') ? view['date_format'] : nil

      tags_color = view.key?('tags_color') ? view['tags_color'] : nil
      tag_filter = false
      if options[:tag]
        tag_filter = { 'tags' => [], 'bool' => 'OR' }
        tag_filter['tags'] = options[:tag].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
        tag_filter['bool'] = options[:bool].normalize_bool
      elsif view.key?('tags') && !(view['tags'].nil? || view['tags'].empty?)
        tag_filter = { 'tags' => [], 'bool' => 'OR' }
        tag_filter['tags'] = if view['tags'].instance_of?(Array)
                               view['tags'].map(&:strip)
                             else
                               view['tags'].gsub(/[, ]+/, ' ').split(' ').map(&:strip)
                             end
        tag_filter['bool'] = view.key?('tags_bool') && !view['tags_bool'].nil? ? view['tags_bool'].normalize_bool : :or
      end

      # If the -o/--output flag was specified, override any default in the view template
      options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'

      count = if options[:c]
                options[:c]
              else
                view.key?('count') ? view['count'] : 10
              end
      section = if options[:s]
                  section
                else
                  view.key?('section') ? view['section'] : settings['current_section']
                end
      order = view.key?('order') ? view['order'].normalize_order : 'asc'

      totals = if options[:totals]
                 true
               else
                 view.key?('totals') ? view['totals'] : false
               end
      tag_order = if options[:tag_order]
                    options[:tag_order].normalize_order
                  else
                    view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
                  end

      options[:t] = true if totals
      output_format = options[:output]&.downcase || 'template'

      options[:sort_tags] = if options[:tag_sort]
                              options[:tag_sort] =~ /^n/i ? true : false
                            elsif view.key?('tag_sort')
                              view['tag_sort'] =~ /^n/i ? true : false
                            else
                              false
                            end
      if view.key?('after') && !options[:after]
        options[:after] = view['after']
      end

      if view.key?('before') && !options[:before]
        options[:before] = view['before']
      end

      if view.key?('from')
        date_string = view['from']
        if date_string =~ / (to|through|thru|(un)?til|-+) /
          dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
          start = wwid.chronify(dates[0], guess: :begin)
          finish = wwid.chronify(dates[2], guess: :end)
        else
          start = wwid.chronify(date_string, guess: :begin)
          finish = false
        end
        raise InvalidTimeExpression, 'Unrecognized date string' unless start
        dates = [start, finish]
      end

      options[:case] = options[:case].normalize_case

      search = nil

      if options[:search]
        search = options[:search]
        search.sub!(/^'?/, "'") if options[:exact]
      end

      opts = options.dup
      opts[:search] = search
      opts[:output] = output_format
      opts[:count] = count
      opts[:format] = date_format
      opts[:highlight] = options[:color]
      opts[:only_timed] = only_timed
      opts[:order] = order
      opts[:section] = section
      opts[:tag_filter] = tag_filter
      opts[:tag_order] = tag_order
      opts[:tags_color] = tags_color
      opts[:template] = template
      opts[:totals] = totals
      opts[:page_title] = page_title
      opts[:date_filter] = dates
      opts[:output] = options[:interactive] ? nil : options[:output]

      Doing::Pager.page wwid.list_section(opts)
    elsif title.instance_of?(FalseClass)
      raise UserCancelled, 'Cancelled' unless res
    else
      raise InvalidView, "View #{title} not found in config"
    end
  end
end

desc 'List available custom views'
command :views do |c|
  c.desc 'List in single column'
  c.switch %i[c column], default_value: false

  c.action do |_global_options, options, _args|
    joiner = options[:c] ? "\n" : "\t"
    print wwid.views.join(joiner)
  end
end

desc 'Move entries between sections'
long_desc %(Argument can be a section name to move all entries from a section,
or start with an "@" to move entries matching a tag.

Default with no argument moves items from the "#{settings['current_section']}" section to Archive.)
arg_name 'SECTION_OR_TAG'
default_value settings['current_section']
command %i[archive move] do |c|
  c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section'
  c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive'
  c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section'
  c.example 'doing move Later --tag project1 --to Currently', desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)'

  c.desc 'How many items to keep (ignored if archiving by tag or search)'
  c.arg_name 'X'
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer

  c.desc 'Move entries to'
  c.arg_name 'SECTION_NAME'
  c.flag %i[t to], default_value: 'Archive'

  c.desc 'Label moved items with @from(SECTION_NAME)'
  c.switch [:label], default_value: true, negatable: true

  c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Tag boolean (AND|OR|NOT)'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Search filter'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Show items that *don\'t* match search string'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Archive entries older than date
    (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
  c.arg_name 'DATE_STRING'
  c.flag [:before]

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    if args.empty?
      section = settings['current_section']
      tags = []
    elsif args[0] =~ /^all/i
      section = 'all'
    elsif args[0] =~ /^@\S+/
      section = 'all'
      tags = args.map { |t| t.sub(/^@/, '').strip }
    else
      section = args[0].cap_first
      tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
    end

    raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]

    tags.concat(options[:tag].to_tags) if options[:tag]

    search = nil

    options[:case] = options[:case].normalize_case

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
    end

    opts = options.dup
    opts[:search] = search
    opts[:bool] = options[:bool].normalize_bool
    opts[:destination] = options[:to]
    opts[:tags] = tags

    wwid.archive(section, opts)
  end
end

desc 'Move entries to archive file'
command :rotate do |c|
  c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
  c.example 'doing rotate --section Archive --keep 10', desc: 'Move entries in the Archive section to a secondary file, keeping the most recent 10 entries'
  c.example 'doing rotate --tag project1,done --bool AND', desc: 'Move entries tagged @project1 and @done to a secondary file'

  c.desc 'How many items to keep in each section (most recent)'
  c.arg_name 'X'
  c.flag %i[k keep], must_match: /^\d+$/, type: Integer

  c.desc 'Section to rotate'
  c.arg_name 'SECTION_NAME'
  c.flag %i[s section], default_value: 'All'

  c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
  c.arg_name 'TAG'
  c.flag [:tag]

  c.desc 'Tag boolean (AND|OR|NOT)'
  c.arg_name 'BOOLEAN'
  c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'

  c.desc 'Search filter'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Rotate items that *don\'t* match search string or tag filter'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Rotate entries older than date
    (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
  c.arg_name 'DATE_STRING'
  c.flag [:before]

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    if options[:section] && options[:section] !~ /^all$/i
      options[:section] = wwid.guess_section(options[:section])
    end

    options[:bool] = options[:bool].normalize_bool

    options[:case] = options[:case].normalize_case

    search = nil

    if options[:search]
      search = options[:search]
      search.sub!(/^'?/, "'") if options[:exact]
      options[:search] = search
    end

    wwid.rotate(options)
  end
end

desc 'Open the "doing" file in an editor'
long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})."
command :open do |c|
  if `uname` =~ /Darwin/
    c.desc 'Open with app name'
    c.arg_name 'APP_NAME'
    c.flag %i[a app]

    c.desc 'Open with app bundle id'
    c.arg_name 'BUNDLE_ID'
    c.flag %i[b bundle_id]
  end

  c.action do |_global_options, options, _args|
    params = options.dup
    params.delete_if do |k, v|
      k.instance_of?(String) || v.nil? || v == false
    end
    if `uname` =~ /Darwin/
      if options[:app]
        system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
      elsif options[:bundle_id]
        system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
      elsif Doing::Util.find_default_editor('doing_file')
        editor = Doing::Util.find_default_editor('doing_file')
        if Doing::Util.exec_available(editor)
          system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
        else
          system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
        end
      else
        system %(open "#{File.expand_path(wwid.doing_file)}")
      end
    else
      raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?

      system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
    end
  end
end

desc 'Edit the configuration file or output a value from it'
long_desc %(Run without arguments, `doing config` opens your `.doingrc` in an editor.
If local configurations are found in the path between the current directory
and `~/.doingrc`, a menu will allow you to select which to open in the editor.

It will use the editor defined in `config_editor_app`, or one specified with `--editor`.

Use `doing config -d` to output the configuration to the terminal, and
provide a dot-separated key path to get a specific value. Shows the current value
including keys/overrides set by local configs.)
arg_name 'KEY_PATH'
command :config do |c|
  c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
  c.example 'doing config -d doing_file', desc: 'Output the value of a config key as YAML'
  c.example 'doing config -d plugins.say.say_voice -o json', desc: 'Output the value of a key path as JSON'

  c.desc 'Editor to use'
  c.arg_name 'EDITOR'
  c.flag %i[e editor], default_value: nil

  c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.'
  c.switch %i[d dump], negatable: false

  c.desc 'Format for --dump (json|yaml|raw)'
  c.arg_name 'FORMAT'
  c.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/

  c.desc 'Update config file with missing configuration options'
  c.switch %i[u update], default_value: false, negatable: false

  if `uname` =~ /Darwin/
    c.desc 'Application to use'
    c.arg_name 'APP_NAME'
    c.flag [:a]

    c.desc 'Application bundle id to use'
    c.arg_name 'BUNDLE_ID'
    c.flag [:b]

    c.desc "Use the config_editor_app defined in ~/.doingrc (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
    c.switch [:x]
  end

  c.action do |_global_options, options, args|
    if options[:update]
      config.configure({rewrite: true, ignore_local: true})
      return
    end

    if options[:dump]
      keypath = args.join('.')
      cfg = config.value_for_key(keypath)

      if cfg
        $stdout.puts case options[:output]
                     when /^j/
                       JSON.pretty_generate(cfg)
                     when /^r/
                       cfg.map {|k, v| v.to_s }
                     else
                       YAML.dump(cfg)
                     end
      else
        Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
      end
      Doing.logger.output_results
      return
    end

    if config.additional_configs.count.positive?
      choices = [config.config_file]
      choices.concat(config.additional_configs)
      res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')

      raise UserCancelled, 'Cancelled' unless res

      config_file = res.strip || config.config_file
    else
      config_file = config.config_file
    end

    if `uname` =~ /Darwin/
      if options[:x]
        editor = Doing::Util.find_default_editor('config')
        if editor
          if Doing::Util.exec_available(editor)
            system %(#{editor} "#{config_file}")
          else
            `open -a "#{editor}" "#{config_file}"`
          end
        else
          raise InvalidArgument, 'No viable editor found in config or environment.'
        end
      elsif options[:a] || options[:b]
        if options[:a]
          `open -a "#{options[:a]}" "#{config_file}"`
        elsif options[:b]
          `open -b #{options[:b]} "#{config_file}"`
        end
      else
        editor = options[:e] || Doing::Util.find_default_editor('config')

        raise MissingEditor, 'No viable editor defined in config or environment' unless editor

        if Doing::Util.exec_available(editor)
          system %(#{editor} "#{config_file}")
        else
          `open -a "#{editor}" "#{config_file}"`
        end
      end
    else
      editor = options[:e] || Doing::Util.default_editor
      raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)

      system %(#{editor} "#{config_file}")
    end
  end
end

desc 'Undo the last change to the doing_file'
command :undo do |c|
  c.desc 'Specify alternate doing file'
  c.arg_name 'PATH'
  c.flag %i[f file], default_value: wwid.doing_file

  c.action do |_global_options, options, _args|
    file = options[:f] || wwid.doing_file
    wwid.restore_backup(file)
  end
end

desc 'Import entries from an external source'
long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
arg_name 'PATH'
command :import do |c|
  c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
  c.arg_name 'TYPE'
  c.flag :type, default_value: 'doing'

  c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
  c.arg_name 'QUERY'
  c.flag [:search]

  # c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
  # c.switch [:fuzzy], default_value: false, negatable: false

  c.desc 'Force exact search string matching (case sensitive)'
  c.switch %i[x exact], default_value: false, negatable: false

  c.desc 'Import items that *don\'t* match search/tag/date filters'
  c.switch [:not], default_value: false, negatable: false

  c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
  c.arg_name 'TYPE'
  c.flag [:case], must_match: /^[csi]/, default_value: 'smart'

  c.desc 'Only import items with recorded time intervals'
  c.switch [:only_timed], default_value: false, negatable: false

  c.desc 'Target section'
  c.arg_name 'NAME'
  c.flag %i[s section]

  c.desc 'Tag all imported entries'
  c.arg_name 'TAGS'
  c.flag :tag

  c.desc 'Autotag entries'
  c.switch :autotag, negatable: true, default_value: true

  c.desc 'Prefix entries with'
  c.arg_name 'PREFIX'
  c.flag :prefix

  c.desc 'Import entries older than date'
  c.arg_name 'DATE_STRING'
  c.flag [:before]

  c.desc 'Import entries newer than date'
  c.arg_name 'DATE_STRING'
  c.flag [:after]

  c.desc %(
    Date range to import. Date range argument should be quoted. Date specifications can be natural language.
    To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
    Has no effect unless the import plugin has implemented date range filtering.
  )
  c.arg_name 'DATE_OR_RANGE'
  c.flag %i[f from]

  c.desc 'Allow entries that overlap existing times'
  c.switch [:overlap], negatable: true

  c.action do |_global_options, options, args|
    options[:fuzzy] = false
    if options[:section]
      options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
    end

    if options[:from]
      date_string = options[:from]
      if date_string =~ / (to|through|thru|(un)?til|-+) /
        dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
        start = wwid.chronify(dates[0], guess: :begin)
        finish = wwid.chronify(dates[2], guess: :end)
      else
        start = wwid.chronify(date_string, guess: :begin)
        finish = false
      end
      raise InvalidTimeExpression, 'Unrecognized date string' unless start
      dates = [start, finish]
    end

    options[:case] = options[:case].normalize_case

    if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
      options[:no_overlap] = !options[:overlap]
      options[:date_filter] = dates
      wwid.import(args, options)
      wwid.write(wwid.doing_file)
    else
      raise InvalidPluginType, "Invalid import type: #{options[:type]}"
    end
  end
end

pre do |global, _command, _options, _args|
  # global[:pager] ||= settings['paginate']

  Doing::Pager.paginate = global[:pager]

  $stdout.puts "doing v#{Doing::VERSION}" if global[:version]
  unless STDOUT.isatty
    Doing::Color::coloring = global[:pager] ? global[:color] : false
  else
    Doing::Color::coloring = global[:color]
  end

  # Return true to proceed; false to abort and not call the
  # chosen command
  # Use skips_pre before a command to skip this block
  # on that command only
  true
end

on_error do |exception|
  if exception.kind_of?(SystemExit)
    false
  else
    # Doing.logger.error('Fatal:', exception)
    Doing.logger.output_results
    true
  end
end

post do |global, _command, _options, _args|
  # Use skips_post before a command to skip this
  # block on that command only
  Doing.logger.output_results
end

around do |global, command, options, arguments, code|
  # Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
  Doing.logger.adjust_verbosity(global)

  if global[:stdout]
    Doing.logger.logdev = $stdout
  end

  wwid.default_option = global[:default]

  if global[:config_file] && global[:config_file] != config.config_file
    Doing.logger.warn(format('%sWARNING:%s %sThe use of --config_file is deprecated, please set the environment variable DOING_CONFIG instead.', colors.flamingo, colors.default, colors.boldred))
    Doing.logger.warn(format('%sTo set it just for the current command, use: %sDOING_CONFIG=/path/to/doingrc doing [command]%s',  colors.red, colors.boldwhite, colors.default))

    cf = File.expand_path(global[:config_file])
    raise MissingConfigFile, "Config file not found (#{global[:config_file]})" unless File.exist?(cf)

    config.config_file = cf
    settings = config.configure({ ignore_local: true })
  end

  if global[:doing_file]
    wwid.init_doing_file(global[:doing_file])
  else
    wwid.init_doing_file
  end

  wwid.auto_tag = !global[:noauto]

  settings[:include_notes] = false unless global[:notes]

  global[:wwid] = wwid

  code.call
end

exit run(ARGV)