#!/usr/bin/env ruby

require 'rubygems'
require 'uri'
require 'tempfile'
require 'trollop'
require 'enumerator'
require "sup"; Redwood::check_library_version_against "0.10"

## save a message 'm' to an open file pointer 'fp'
def save m, fp
  m.source.each_raw_message_line(m.source_info) { |l| fp.print l }
end
def die msg
  $stderr.puts "Error: #{msg}"
  exit(-1)
end
def has_any_from_source_with_label? index, source, label
  query = { :source_id => source.id, :label => label, :limit => 1, :load_spam => true, :load_deleted => true, :load_killed => true }
  not Enumerable::Enumerator.new(index, :each_id, query).map.empty?
end

opts = Trollop::options do
  version "sup-sync-back (sup #{Redwood::VERSION})"
  banner <<EOS
Drop or move messages from Sup sources that are marked as deleted or
spam in the Sup index.

Currently only works with mbox sources.

Usage:
  sup-sync-back [options] <source>*

where <source>* is zero or more source URIs. If no sources are given,
sync back all usual sources.

You almost certainly want to run sup-sync --changed after this command.
Running this does not change the index.

Options include:
EOS
  opt :drop_deleted, "Drop deleted messages.", :default => false, :short => "d"
  opt :move_deleted, "Move deleted messages to a local mbox file.", :type => String, :short => :none
  opt :drop_spam, "Drop spam messages.", :default => false, :short => "s"
  opt :move_spam, "Move spam messages to a local mbox file.", :type => String, :short => :none

  opt :with_dotlockfile, "Specific dotlockfile location (mbox files only).", :default => "/usr/bin/dotlockfile", :short => :none
  opt :dont_use_dotlockfile, "Don't use dotlockfile to lock mbox files. Dangerous if other processes modify them concurrently.", :default => false, :short => :none

  opt :index, "Use this index type ('auto' for autodetect)", :default => "auto"
  opt :verbose, "Print message ids as they're processed."
  opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
  opt :version, "Show version information", :short => :none

  conflicts :drop_deleted, :move_deleted
  conflicts :drop_spam, :move_spam
end

unless opts[:drop_deleted] || opts[:move_deleted] || opts[:drop_spam] || opts[:move_spam]
  puts <<EOS
Nothing to do. Please specify at least one of --drop-deleted, --move-deleted,
--drop-spam, or --move-spam.
EOS

  exit
end

Redwood::start
index = Redwood::Index.init opts[:index]
index.lock_interactively or exit

deleted_fp, spam_fp = nil
unless opts[:dry_run]
  deleted_fp = File.open(opts[:move_deleted], "a") if opts[:move_deleted] 
  spam_fp = File.open(opts[:move_spam], "a") if opts[:move_spam]
end

dotlockfile = opts[:with_dotlockfile] || "/usr/bin/dotlockfile"

begin
  index.load

  sources = ARGV.map do |uri|
    s = Redwood::SourceManager.source_for(uri) or die "unknown source: #{uri}. Did you add it with sup-add first?"
    s.is_a?(Redwood::MBox::Loader) or die "#{uri} is not an mbox source."
    s
  end

  if sources.empty?
    sources = Redwood::SourceManager.usual_sources.select { |s| s.is_a? Redwood::MBox::Loader }
  end

  unless sources.all? { |s| s.file_path.nil? } || File.executable?(dotlockfile) || opts[:dont_use_dotlockfile]
    die <<EOS
can't execute dotlockfile binary: #{dotlockfile}. Specify --with-dotlockfile
if it's in a nonstandard location, or, if you want to live dangerously, try
--dont-use-dotlockfile
EOS
  end

  modified_sources = []
  sources.each do |source|
    $stderr.puts "Scanning #{source}..."

    unless ((opts[:drop_deleted] || opts[:move_deleted]) && has_any_from_source_with_label?(index, source, :deleted)) || ((opts[:drop_spam] || opts[:move_spam]) && has_any_from_source_with_label?(index, source, :spam))
      $stderr.puts "Nothing to do from this source; skipping"
      next
    end

    source.reset!
    num_dropped = num_moved = num_scanned = 0

    out_fp = Tempfile.new "sup-sync-back-#{source.id}"
    Redwood::PollManager.each_message_from source do |m|
      num_scanned += 1

      if(m_old = index.build_message(m.id))
        labels = m_old.labels

        if labels.member? :deleted
          if opts[:drop_deleted]
            puts "Dropping deleted message #{source}##{m.source_info}" if opts[:verbose]
            num_dropped += 1
          elsif opts[:move_deleted] && labels.member?(:deleted)
            puts "Moving deleted message #{source}##{m.source_info}" if opts[:verbose]
            save m, deleted_fp unless opts[:dry_run]
            num_moved += 1
          end

        elsif labels.member? :spam
          if opts[:drop_spam]
            puts "Dropping spam message #{source}##{m.source_info}" if opts[:verbose]
            num_dropped += 1
          elsif opts[:move_spam] && labels.member?(:spam)
            puts "Moving spam message #{source}##{m.source_info}" if opts[:verbose]
            save m, spam_fp unless opts[:dry_run]
            num_moved += 1
          end
        else
          save m, out_fp unless opts[:dry_run]
        end
      else
        save m, out_fp unless opts[:dry_run]
      end
    end
    $stderr.puts "Scanned #{num_scanned}, dropped #{num_dropped}, moved #{num_moved} messages from #{source}."
    modified_sources << source if num_dropped > 0 || num_moved > 0
    out_fp.close unless opts[:dry_run]

    unless opts[:dry_run] || (num_dropped == 0 && num_moved == 0)
      deleted_fp.flush if deleted_fp
      spam_fp.flush if spam_fp
      unless opts[:dont_use_dotlockfile]
        puts "Locking #{source.file_path}..."
        system "#{opts[:dotlockfile]} -l #{source.file_path}"
        puts "Writing #{source.file_path}..."
        FileUtils.cp out_fp.path, source.file_path
        puts "Unlocking #{source.file_path}..."
        system "#{opts[:dotlockfile]} -u #{source.file_path}"
      end
    end
  end

  unless opts[:dry_run]
    deleted_fp.close if deleted_fp
    spam_fp.close if spam_fp
  end

  $stderr.puts "Done."
  unless modified_sources.empty?
    $stderr.puts "You should now run: sup-sync --changed #{modified_sources.join(' ')}"
  end
rescue Exception => e
  File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
  raise
ensure
  Redwood::finish
  index.unlock
end