#!/usr/bin/perl

# Copyright © 2020-2021 Felix Lechner
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

use v5.20;
use warnings;
use utf8;

use Const::Fast;
use Data::Compare;
use DBI;
use IPC::Run3;
use List::Compare;
use JSON::MaybeXS;
use List::SomeUtils qw(uniq all);
use Path::Tiny;
use Text::Markdown::Discount qw(markdown);
use Time::Duration;
use Unicode::UTF8 qw(encode_utf8 decode_utf8);
use YAML::XS qw(LoadFile);

const my $EMPTY => q{};
const my $SPACE => q{ };
const my $INDENT => $SPACE x 4;
const my $HYPHEN => q{-};

die encode_utf8("Usage [config-file] ( [lintian-version] )+\n")
  if @ARGV <= 1;

my $configfile = shift @ARGV;
die encode_utf8('No config file')
  unless length $configfile;

# load config file
my $yaml = LoadFile($configfile);

# get database config
my $dbconnectstring = $yaml->{database};
die encode_utf8('No database connect string')
  unless length $dbconnectstring;

my $postgres = DBI->connect('dbi:Pg:' . $dbconnectstring,
    $EMPTY, $EMPTY,{AutoCommit => 0, pg_enable_utf8 => 0});

my @requested_versions = map { decode_utf8($_) } @ARGV;

my $check_lintian_versions_sql =<<~'END_OF_QUERY';
SELECT lintian_version
FROM lintian.versions
WHERE lintian_version = ANY($1)
END_OF_QUERY

say encode_utf8('Looking for the following Lintian versions:');
say encode_utf8($INDENT . $HYPHEN . $SPACE . $_) for @requested_versions;

my @have_versions;
my $check_lintian_versions= $postgres->prepare($check_lintian_versions_sql);
$check_lintian_versions->execute(
    [map { encode_utf8($_) } @requested_versions]);
while (my $row = $check_lintian_versions->fetchrow_hashref) {
    push(@have_versions, decode_utf8($row->{lintian_version}));
}
$check_lintian_versions->finish;
$postgres->commit;

my $lintian_lc
  = List::Compare->new([map { decode_utf8($_) } @ARGV], \@have_versions);
my @missing_versions = $lintian_lc->get_Lonly;

add_lintian_tags($postgres, $_) for @missing_versions;

$postgres->disconnect;

my $elapsed_seconds = time - $^T;
say encode_utf8('Elapsed time: ' . duration($elapsed_seconds));

exit;

sub add_lintian_tags {
    my ($database, $version) = @_;

    my ($major, $minor, $patch) = split(/[.]/, $version);

    die encode_utf8("Cannot parse Lintian version string $version")
      unless all { defined } ($major, $minor, $patch);

    say encode_utf8("Pulling in tags for Lintian version $version from Git.");

    my $tempdir = Path::Tiny->tempdir(TEMPLATE => 'addtags-XXXXXXXX');
    die encode_utf8('Cannot clone Lintian repo')
      if system(
"git -C $tempdir clone https://salsa.debian.org/lintian/lintian.git lintian"
      );

    # closest recent release
    my $committish = "$major.$minor.0";

    if ($patch > 0) {
        my $output;

        my @command = (
            'git', '-C',
            "$tempdir/lintian", 'rev-list',
            '--no-merges', "$committish..HEAD"
        );
        run3(\@command, \undef, \$output, undef);

        die encode_utf8("Cannot get commit list for Lintian version $version")
          if $?;

        my @hashes = reverse split(/\n/, $output);

        die encode_utf8(
            "Not enough commit hashes for Lintian version $version")
          if $patch > @hashes;

        $committish = $hashes[$patch - 1];
    }

    die encode_utf8("Cannot reset Lintian repo to $committish")
      if system("git -C $tempdir/lintian reset --hard $committish");

    my $add_version_sql =<<~'END_OF_QUERY';
        INSERT INTO lintian.versions (lintian_version, repository_reference)
        VALUES ($1, $2)
        ON CONFLICT (lintian_version)
            DO NOTHING
    END_OF_QUERY

    my $add_version = $database->prepare($add_version_sql);
    $add_version->execute(encode_utf8($version), encode_utf8($committish));

    my $explaintags = "$tempdir/lintian/bin/lintian-explain-tags";
    my $list_out;

    run3([$explaintags, qw(--format json)], undef, \$list_out);

    my $definitions = decode_json($list_out);
    my @sorted = sort { lc($a->{name}) cmp lc($b->{name}) } @{$definitions};

    my @db_tags;
    my @db_renamed_tags;
    my @db_screens;

    for my $tag (@sorted) {

        my $tag_name = $tag->{name};
        $tag->{tag_name} = $tag_name;
        delete $tag->{name};

        $tag->{experimental} //= 0;
        $tag->{show_always} //= 0;
        $tag->{name_spaced} //= 0;

        $tag->{explanation_html} = markdown($tag->{explanation});
        delete $tag->{explanation};

        my @html_see_also
          = map { markdown_to_stripped_html($_) } @{$tag->{see_also} // [] };
        $tag->{see_also_html} = \@html_see_also;
        delete $tag->{see_also};

        push(@db_tags, $tag);

        for my $previous_name (@{$tag->{renamed_from} // []}) {

            my %db_renamed_tag;
            $db_renamed_tag{renamed_from} = $previous_name;
            $db_renamed_tag{tag_name} = $tag->{tag_name};
            $db_renamed_tag{lintian_version} = $tag->{lintian_version};

            push(@db_renamed_tags, \%db_renamed_tag);
        }

        for my $screen (@{$tag->{screens} // []}) {

            $screen->{screen} = $screen->{name};
            delete $screen->{name};

            $screen->{tag_name} = $tag->{tag_name};
            $screen->{lintian_version} = $tag->{lintian_version};

            $screen->{reason_html} = markdown($screen->{reason});
            delete $screen->{reason};

            my @html_screen_see_also = map { markdown_to_stripped_html($_) }
              @{$screen->{see_also} // [] };
            $screen->{see_also_html} = \@html_screen_see_also;
            delete $screen->{see_also};

            push(@db_screens, $screen);
        }

        delete $tag->{screens};
    }

    my $synchronize_tags_sql =<<~'END_OF_QUERY';
        WITH data AS (
            SELECT *
            FROM json_populate_recordset(null::lintian.tags, $1)
        ),
        d AS (
            DELETE FROM lintian.tags AS t
            WHERE lintian_version = $2
            AND NOT EXISTS (
                SELECT * FROM data
                WHERE data.tag_name = t.tag_name
                AND data.lintian_version = t.lintian_version
            )
        )
        INSERT INTO lintian.tags
        SELECT * FROM data
        ON CONFLICT (tag_name, lintian_version)
            DO NOTHING
    END_OF_QUERY

    my $tags_json_array = encode_json(\@db_tags);
    my $synchronize_tags = $database->prepare($synchronize_tags_sql);
    $synchronize_tags->execute($tags_json_array, encode_utf8($version));

    my $synchronize_renamed_tags_sql =<<~'END_OF_QUERY';
        WITH data AS (
            SELECT *
            FROM json_populate_recordset(null::lintian.renamed_tags, $1)
        ),
        d AS (
            DELETE FROM lintian.renamed_tags AS t
            WHERE lintian_version = $2
            AND NOT EXISTS (
                SELECT * FROM data
                WHERE data.renamed_from = t.renamed_from
                AND data.lintian_version = t.lintian_version
            )
        )
        INSERT INTO lintian.renamed_tags
        SELECT * FROM data
        ON CONFLICT (renamed_from, lintian_version)
            DO NOTHING
    END_OF_QUERY

    my $renamed_tags_json_array = encode_json(\@db_renamed_tags);
    my $synchronize_renamed_tags
      = $database->prepare($synchronize_renamed_tags_sql);
    $synchronize_renamed_tags->execute($renamed_tags_json_array,
        encode_utf8($version));

    my $synchronize_screens_sql =<<~'END_OF_QUERY';
        WITH data AS (
            SELECT *
            FROM json_populate_recordset(null::lintian.screens, $1)
        ),
        d AS (
            DELETE FROM lintian.screens AS s
            WHERE lintian_version = $2
            AND NOT EXISTS (
                SELECT * FROM data
                WHERE data.screen = s.screen
                AND data.lintian_version = s.lintian_version
            )
        )
        INSERT INTO lintian.screens
        SELECT * FROM data
        ON CONFLICT (screen, lintian_version)
            DO NOTHING
    END_OF_QUERY

    my $screens_json_array = encode_json(\@db_screens);
    my $synchronize_screens= $database->prepare($synchronize_screens_sql);
    $synchronize_screens->execute($screens_json_array,encode_utf8($version));

    # add manual
    my $manual_utf8;

    # for rst2html
    local $ENV{LC_ALL} = 'en_US.UTF-8';

    run3(['rst2html', "$tempdir/lintian/doc/lintian.rst"], undef, \$manual_utf8);
    my $manual_unicode = decode_utf8($manual_utf8);

    my %db_manual_page;
    $db_manual_page{path} = 'index.html';
    $db_manual_page{html} = $manual_unicode;
    $db_manual_page{lintian_version} = $version;

    my @db_manual_pages;
    push(@db_manual_pages, \%db_manual_page);

    my $synchronize_manual_pages_sql =<<~'END_OF_QUERY';
        WITH data AS (
            SELECT *
            FROM json_populate_recordset(null::lintian.manual_pages, $1)
        ),
        d AS (
            DELETE FROM lintian.manual_pages AS mp
            WHERE lintian_version = $2
            AND NOT EXISTS (
                SELECT * FROM data
                WHERE data.path = mp.path
                AND data.lintian_version = mp.lintian_version
            )
        )
        INSERT INTO lintian.manual_pages
        SELECT * FROM data
        ON CONFLICT (path, lintian_version)
            DO NOTHING
    END_OF_QUERY

    my $manual_pages_json_array = encode_json(\@db_manual_pages);
    my $synchronize_manual_pages= $database->prepare($synchronize_manual_pages_sql);
    $synchronize_manual_pages->execute($manual_pages_json_array,encode_utf8($version));

    $database->commit;

    return;
}

sub markdown_to_stripped_html {
    my ($markdown) = @_;

    my $html = markdown($markdown);

    # discount library wraps with <p> tag and adds newline
    chomp $html;
    $html =~ s{^ <p> }{}x;
    $html =~ s{ </p> $}{}x;

    return $html;
}

# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 sr et
