package Cpanel::CCS::DBUtils;

# cpanel - Cpanel/CCS/DBUtils.pm                   Copyright 2020 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use strict;
use warnings;

use lib '/var/cpanel/perl';

use DBD::Pg;

use Cpanel::CCS::Constants ();

our $sockdir = Cpanel::CCS::Constants::CCS_INSTALL_DIR . '/data/Data/Database/psqlsocks';
my $dbh;

use constant {

    # see _insert_collection_if_needed_for_type()
    CHECK_MAP => {
        'calendars' => "SELECT resource_id FROM calendar_home WHERE owner_uid = ?;",
        'contacts'  => "SELECT resource_id FROM addressbook_home WHERE owner_uid = ?;",
    }
};

=head1 NAME

Cpanel::CCS::DBUtils

=head1 SYNOPSIS

my $dbh = Cpanel::CCS::DBUtils::get_dbh();

=head1 DESCRIPTION

Module for getting a DBD::Pg object which is connected to the CCS Database.

=head1 SUBROUTINES

=head2 get_dbh

Returns the database handle object for the CCS postgres cluster.

=cut

sub get_dbh {
    return $dbh if $dbh;
    my $dbh = DBI->connect( "dbi:Pg:dbname=caldav;host=$sockdir", 'caldav', undef, { 'RaiseError' => 1 } );
    die "Couldn't connect to Postgres" . ( $DBI::errstr ? ': ' . $DBI::errstr : '' ) if !$dbh;

    return $dbh;
}

sub get_restorepkg_query_for_type {
    my ( $type, $resource_id, $uuid, $full_text ) = @_;

    require Digest::MD5;
    my $md5 = Digest::MD5::md5_hex($full_text);

    # Note what I'm not doing here -- upserts. They aren't really allowed
    # in the pg version we ship, so I'm basically stuck doing it the CTE way
    # (if I do it at all), and that's not exactly great either:
    # https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql#answer-8702291
    # https://dba.stackexchange.com/questions/78510/why-is-cte-open-to-lost-updates
    # Still, I guess it is "better than nothing".
    my %query_map = (
        'calendars' => {
            'query' => "WITH new_values ( calendar_resource_id, resource_name, icalendar_text, icalendar_uid, icalendar_type, md5, dataversion )
                        AS ( VALUES ( CAST( ? AS bigint ), ?, ?, ?, ?, ?, CAST( ? AS int ) ) ),
                        upsert AS (
                            UPDATE calendar_object
                            SET icalendar_text= ?, md5 = ?
                            FROM new_values
                            WHERE calendar_object.icalendar_uid = ? AND calendar_object.calendar_resource_id = ?
                            RETURNING calendar_object.*
                        )
                        INSERT INTO calendar_object ( calendar_resource_id, resource_name, icalendar_text, icalendar_uid, icalendar_type, md5, dataversion )
                        SELECT calendar_resource_id, resource_name, icalendar_text, icalendar_uid, icalendar_type, md5, dataversion
                        FROM new_values
                        WHERE NOT EXISTS ( SELECT 1 FROM upsert WHERE upsert.icalendar_uid = new_values.icalendar_uid AND upsert.calendar_resource_id = new_values.calendar_resource_id );",
            'args' => [ $resource_id, "$uuid.ics", $full_text, $uuid, 'VEVENT', $md5, 1, $full_text, $md5, $uuid, $resource_id ],
        },
        'contacts' => {
            'query' => "WITH new_values ( addressbook_home_resource_id, resource_name, vcard_text, vcard_uid, kind, md5 )
                        AS ( VALUES ( CAST( ? as bigint ), ?, ?, ?, CAST( ? AS int ), ? ) ),
                        upsert AS (
                            UPDATE addressbook_object
                            SET vcard_text= ?, md5 = ?
                            FROM new_values
                            WHERE addressbook_object.vcard_uid = ? AND addressbook_object.addressbook_home_resource_id = ?
                            RETURNING addressbook_object.*
                        )
                        INSERT INTO addressbook_object ( addressbook_home_resource_id, resource_name, vcard_text, vcard_uid, kind, md5 )
                        SELECT addressbook_home_resource_id, resource_name, vcard_text, vcard_uid, kind, md5
                        FROM new_values
                        WHERE NOT EXISTS ( SELECT 1 FROM upsert WHERE upsert.vcard_uid = new_values.vcard_uid AND upsert.addressbook_home_resource_id = new_values.addressbook_home_resource_id );",
            'args' => [ $resource_id, "$uuid.vcard", $full_text, $uuid, 0, $md5, $full_text, $md5, $uuid, $resource_id ],
        },
    );
    return ( $query_map{$type}->{'query'}, @{ $query_map{$type}->{'args'} } );
}

=head2 do_restorepkg_inserts_for_type

Subroutine for inserting event or vcard data.

=cut

# File can also be a string ref. If a string ref, just read it.
sub do_restorepkg_inserts_for_type {
    my ( $type, $dbh, $file, $resource_id, $uuid ) = @_;

    Cpanel::Autodie::open( my $fh, "<", $file );

    # Thankfully no nesting exists
    my $inside_event;
    my $evt_uuid = "";
    my %type_map = (
        'calendars' => 'VCALENDAR',
        'contacts'  => 'VCARD',
    );

    # Do it all in txn so that it doesn't take so long when lots
    # of events must be processed.
    $dbh->begin_work();

    my $full_text  = '';
    my $inserts = 0;
    while (<$fh>) {

        # Check if we're starting an event. If not, ignore the line.
        if ( !$inside_event ) {
            $inside_event = ( index( $_, "BEGIN:$type_map{$type}" ) == 0 );
        }
        next if !$inside_event;

        # Append the line to th text blob we're building to insert.
        # Also check if we're actually at the end of this item.
        $full_text .= $_;
        ($evt_uuid) = $_ =~ m/^UID:(.*)\r/ if index( $_, "UID:" ) == 0;
        if ($inside_event) {
            $inside_event = 0 if index( $_, "END:$type_map{$type}" ) == 0;

            # if so, run the insert.
            if ( !$inside_event ) {

                # Construct the INSERT query and execute it.
                my ( $query, @args ) = Cpanel::CCS::DBUtils::get_restorepkg_query_for_type( $type, $resource_id, $evt_uuid, $full_text );
                $dbh->do( $query, undef, @args );
                $inserts++;

                # Reset $full_text so that we can process the next entry
                $full_text = '';
            }
        }
    }
    $dbh->commit;
    close $fh;

    print "Added $inserts record(s) for $type corresponding to $uuid\n";
    return;
}

=head2 insert_collection_if_needed_for_type

Inserts an entry into the database for the calendar collection if needed
Called during pkgacct_restore.

=cut

sub insert_collection_if_needed_for_type {
    my ( $type, $dbh, $uuid ) = @_;

    # Do the query to add the "home" for collections, run it
    my ( $calendar_home_resource_id, $resource_id );

    # But first check whether the resource already exists
    my $sth  = $dbh->prepare( CHECK_MAP->{$type} );
    my $rows = $sth->execute($uuid);

    # Add it if it does not exist
    # 0 rows from DBD::Pg is "0 yet truthy -- 0E0"
    if ( $rows == 0 ) {
        my ( $query, @args ) = _get_coll_add_query_for_type( $type, $uuid );
        $sth  = $dbh->prepare($query);
        $rows = $sth->execute(@args);
    }

    # Now get the home_resource_id
    die "Query for resource_id of $uuid returned no rows! OOPS"             if !$rows;
    warn "Query for resource_id of $uuid returned more than one row! UH OH" if $rows > 1;

    my $inserted_into_calendar = 0;
    if ( $type eq 'calendars' ) {

        # Capture the calendar home resource ID, as we'll need it later.
        ($calendar_home_resource_id) = $sth->fetchrow_array();

        # Also gotta insert a meta table for calendar_home (sigh)
        if ( $dbh->do( "SELECT resource_id FROM calendar_home_metadata WHERE resource_id = ?;", undef, $calendar_home_resource_id ) == 0 ) {
            $dbh->do(
                "INSERT INTO calendar_home_metadata ( resource_id ) VALUES ( ? );",
                undef,
                $calendar_home_resource_id,
            );
        }

        # Check for any collections underneath that calendar_home. If none
        # exist, then make one.
        $sth = $dbh->prepare(
            "SELECT calendar_resource_id
             FROM calendar_bind
             WHERE calendar_home_resource_id = ?
             AND calendar_resource_name = ?;"
        );
        $rows = $sth->execute( $calendar_home_resource_id, 'calendar' );

        if ( $rows == 0 ) {
            $inserted_into_calendar = 1;

            # I specifically want DEFAULT to be a bareword here (unquoted).
            $sth = $dbh->prepare(
                'INSERT INTO calendar ( resource_id )
                 VALUES ( DEFAULT )
                 RETURNING resource_id;'
            );
            $sth->execute();
        }
    }

    # Get the resource ID
    ($resource_id) = $sth->fetchrow_array();

    # Also insert a record into calendar_bind, as that's where this stuff
    # gets glued together (if we need to, anyways)
    if ( $type eq 'calendars' && $inserted_into_calendar == 1 ) {
        $sth = $dbh->prepare(
            'INSERT INTO calendar_bind ( calendar_home_resource_id, calendar_resource_id, calendar_resource_name, bind_mode, bind_status )
             VALUES ( ?, ?, ?, ?, ? );'
        );
        $sth->execute( $calendar_home_resource_id, $resource_id, 'calendar', 0, 1 );
    }

    # Now, since CCS apparently likes repeating itself, we have to insert into
    # a metadata table with the same resource ID.

    my $tbl_prefix = ( $type eq 'calendars' ) ? 'calendar' : 'addressbook_home';
    $sth = $dbh->prepare(
        "SELECT resource_id
         FROM ${tbl_prefix}_metadata
         WHERE resource_id = ?;"
    );
    $rows = $sth->execute($resource_id);
    if ( $rows == 0 ) {
        my @extra_cal = ( ', supported_components', ', ?' );
        $sth = $dbh->prepare( "INSERT INTO ${tbl_prefix}_metadata ( resource_id" . ( $type eq 'calendars' ? $extra_cal[0] : "" ) . " )" . "VALUES ( ?" . ( $type eq 'calendars' ? $extra_cal[1] : "" ) . " );" );
        my @vars = ($resource_id);
        push( @vars, 'VEVENT' ) if $type eq 'calendars';
        $sth->execute(@vars);
    }

    return $resource_id;
}

sub _get_coll_add_query_for_type {
    my ( $type, $uuid ) = @_;
    my %query_map = (
        'calendars' => {
            'query' => "INSERT INTO calendar_home
                        ( owner_uid, status, dataversion )
                        VALUES
                        ( ?, ?, ? )
                        RETURNING resource_id",
            'args' => [ $uuid, 0, 6 ],
        },
        'contacts' => {
            'query' => "INSERT INTO addressbook_home
                        ( owner_uid, status, dataversion )
                        VALUES
                        ( ?, ?, ? )
                        RETURNING resource_id;",
            'args' => [ $uuid, 0, 2 ],
        },
    );
    return ( $query_map{$type}->{'query'}, @{ $query_map{$type}->{'args'} } );
}

=head2 scrub_data_for_resource_id

Cleans out any previous vcard & ics data for a uuid in the DB.

=cut

sub scrub_data_for_uuid {
    my ( %opts ) = @_;
    return warn "Missing opts to scrub_data_for_uuid!" if scalar( grep { $opts{$_} } qw{resource_id type dbh uuid} ) != 4;
    my %queries = (
        'calendars' => "DELETE FROM calendar_object WHERE calendar_resource_id=?",
        'contacts' => "DELETE FROM addressbook_object WHERE addressbook_home_resource_id=?",
    );
    my $sth = $opts{'dbh'}->prepare($queries{$opts{'type'}});
    my $rows = $sth->execute( $opts{'resource_id'} );
    print "Removed $rows old record(s) for $opts{'type'} corresponding to $opts{'uuid'}\n" if $rows;

    return;
}

1;
