# Copyright (C) all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>

# like PublicInbox::SearchIdx, but for searching for non-mail messages.
# Things indexed include:
# * inboxes themselves
# * epoch information
# * (maybe) git code repository information (not commits)
# Expect ~100K-1M documents with no parallelism opportunities,
# so no sharding, here.
#
# See MiscSearch for read-only counterpart
package PublicInbox::MiscIdx;
use strict;
use v5.10.1;
use PublicInbox::InboxWritable;
use PublicInbox::Search; # for SWIG Xapian and Search::Xapian compat
use PublicInbox::SearchIdx qw(index_text term_generator add_val add_bool_term);
use Carp qw(croak);
use File::Path ();
use PublicInbox::MiscSearch;
use PublicInbox::Config;
use PublicInbox::Syscall;
my $json;

sub new {
	my ($class, $eidx) = @_;
	PublicInbox::SearchIdx::load_xapian_writable();
	my $mi_dir = "$eidx->{xpfx}/misc";
	File::Path::mkpath($mi_dir);
	my $opt = $eidx->{-opt};
	$opt->{cow} or
		PublicInbox::Syscall::nodatacow_dir($mi_dir);
	my $flags = $PublicInbox::SearchIdx::DB_CREATE_OR_OPEN;
	$flags |= $PublicInbox::SearchIdx::DB_NO_SYNC if !$opt->{fsync};
	$flags |= $PublicInbox::SearchIdx::DB_DANGEROUS if $opt->{dangerous};
	$json //= PublicInbox::Config::json();
	bless {
		mi_dir => $mi_dir,
		flags => $flags,
		indexlevel => 'full', # small DB, no point in medium?
	}, $class;
}

sub _begin_txn ($) {
	my ($self) = @_;
	my $wdb = $PublicInbox::Search::X{WritableDatabase};
	my $xdb = eval { $wdb->new($self->{mi_dir}, $self->{flags}) };
	croak "Failed opening $self->{mi_dir}: $@" if $@;
	$xdb->begin_transaction;
	$xdb;
}

sub commit_txn {
	my ($self) = @_;
	my $xdb = delete $self->{xdb} or return;
	$xdb->commit_transaction;
}

sub create_xdb {
	my ($self) = @_;
	$self->{xdb} //= _begin_txn($self);
	commit_txn($self);
}

sub remove_eidx_key {
	my ($self, $eidx_key) = @_;
	my $xdb = $self->{xdb} //= _begin_txn($self);
	my $head = $xdb->postlist_begin('Q'.$eidx_key);
	my $tail = $xdb->postlist_end('Q'.$eidx_key);
	my @docids; # only one, unless we had bugs
	for (; $head != $tail; $head++) {
		push @docids, $head->get_docid;
	}
	for my $docid (@docids) {
		$xdb->delete_document($docid);
		warn "# remove inbox docid #$docid ($eidx_key)\n";
	}
}

# adds or updates according to $eidx_key
sub index_ibx {
	my ($self, $ibx) = @_;
	my $eidx_key = $ibx->eidx_key;
	my $xdb = $self->{xdb} //= _begin_txn($self);
	# Q = uniQue in Xapian terminology
	my $head = $xdb->postlist_begin('Q'.$eidx_key);
	my $tail = $xdb->postlist_end('Q'.$eidx_key);
	my ($docid, @drop);
	for (; $head != $tail; $head++) {
		if (defined $docid) {
			my $i = $head->get_docid;
			push @drop, $i;
			warn <<EOF;
W: multiple inboxes keyed to `$eidx_key', deleting #$i
EOF
		} else {
			$docid = $head->get_docid;
		}
	}
	$xdb->delete_document($_) for @drop; # just in case

	my $doc = $PublicInbox::Search::X{Document}->new;
	term_generator($self)->set_document($doc);

	# allow sorting by modified and uidvalidity (created at)
	add_val($doc, $PublicInbox::MiscSearch::MODIFIED, $ibx->modified);
	add_val($doc, $PublicInbox::MiscSearch::UIDVALIDITY, $ibx->uidvalidity);

	add_bool_term($doc, 'Q'.$eidx_key); # uniQue id
	$doc->add_boolean_term('T'.'inbox'); # Type

	# force reread from disk, {description} could be loaded from {misc}
	delete @$ibx{qw(-art_min -art_max description)};
	if (defined($ibx->{newsgroup}) && $ibx->nntp_usable) {
		$doc->add_boolean_term('T'.'newsgroup'); # additional Type
		my $n = $ibx->art_min;
		add_val($doc, $PublicInbox::MiscSearch::ART_MIN, $n) if $n;
		$n = $ibx->art_max;
		add_val($doc, $PublicInbox::MiscSearch::ART_MAX, $n) if $n;
	}

	my $desc = $ibx->description;

	# description = S/Subject (or title)
	# address = A/Author
	index_text($self, $desc, 1, 'S');
	index_text($self, $ibx->{name}, 1, 'XNAME');
	my %map = (
		address => 'A',
		listid => 'XLISTID',
		infourl => 'XINFOURL',
		url => 'XURL'
	);
	while (my ($f, $pfx) = each %map) {
		for my $v (@{$ibx->{$f} // []}) {
			index_text($self, $v, 1, $pfx);
		}
	}
	my $data = {};
	if (defined(my $max = $ibx->max_git_epoch)) { # v2
		my $pfx = "/$ibx->{name}/git/";
		for my $epoch (0..$max) {
			my $git = $ibx->git_epoch($epoch) or return;
			if (my $ent = $git->manifest_entry($epoch, $desc)) {
				$data->{"$pfx$epoch.git"} = $ent;
				$ent->{git_dir} = $git->{git_dir};
			}
			$git->cleanup; # ->modified starts cat-file --batch
		}
	} elsif (my $ent = $ibx->git->manifest_entry) { # v1
		$ent->{git_dir} = $ibx->{inboxdir};
		$data->{"/$ibx->{name}"} = $ent;
	}
	$doc->set_data($json->encode($data));
	if (defined $docid) {
		$xdb->replace_document($docid, $doc);
	} else {
		$xdb->add_document($doc);
	}
}

1;
