#!/usr/bin/perl
use strict;

use Proc::Pidfile;
use Text::ParseWords;
use Data::Dumper;

use lib '/app/tools/common/lib';
use Common::RSApp;
use Common::Log;
use Common::DB::Item::TableChangeLog;
use Common::DB::Item::Client;
use Common::DB::Item::ClientType;
use Common::DB::Item::ClientCMIDMap;

use lib '/app/tools/rps/lib';
use RPS::DB::Item::Track;
use RPS::DB::Item::Album;
use RPS::DB::Item::Product;
use RPS::DB::Item::Master;
use RPS::DB::Item::Song;
use RPS::DB::Item::Artist;
use RPS::DB::Item::Composer;
use RPS::DB::Item::Label;
use RPS::DB::Item::ProductTrack;

use constant kCMClientID => 112;

# Create a pidfile.  This is just a file that will contain this script's
# process id.  We'll use the Proc::Pidfile module - This will allow
# us to silently exit if the file already exists (so we don't run two instances
# of this script at once), and will automatically clean up the pid file.
#
use constant kPIDFile => '/tmp/consumeMetadataChangeLog.pid';
my $pidfile = Proc::Pidfile->new( pidfile => kPIDFile, silent => 1 );

# Redirect stderr to a log file.
#
use constant kLogFilePath => '/app/data/logs/consumeMetadataChangeLog.log';
open( STDERR, ">> " . kLogFilePath ) or die "cannot redirect STDERR: $!\n";
select STDERR;
$| = 1;
select STDOUT;
$| = 1;

my $gErrors;

# This will map table name to constructor, and any additional state we
# might need to keep track of.
#
my %gTableMap = (
    'track' => {
        class     => 'RPS::DB::Item::Track',
        autoField => 'track_id',
    },
    'album' => {
        class     => 'RPS::DB::Item::Album',
        autoField => 'album_id',
    },
    'product' => {
        class     => 'RPS::DB::Item::Product',
        autoField => 'product_id',
    },
    'master' => {
        class     => 'RPS::DB::Item::Master',
        autoField => 'master_id',
    },
    'song' => {
        class     => 'RPS::DB::Item::Song',
        autoField => 'song_id',
    },
    'artist' => {
        class     => 'RPS::DB::Item::Artist',
        autoField => 'artist_id',
        noDelete  => 1,
        uniqueKey => 'name',
    },
    'composer' => {
        class     => 'RPS::DB::Item::Composer',
        autoField => 'composer_id',
        noDelete  => 1,
        uniqueKey => 'name',
    },
    'label' => {
        class     => 'RPS::DB::Item::Label',
        autoField => 'label_id',
    },
    'product_track' => {
        class     => 'RPS::DB::Item::ProductTrack',
        autoField => 'product_track_id',

        # No autoincrement key for this table
    },
);

my $singleton = Common::RSApp->new( clientID => kCMClientID );

# Make a map of valid client ids.
#
my %gDistClientIDs;
my $distClients = Common::DB::Item::Client->GetAllOfType(Common::DB::Item::ClientType::kClientTypeBitMaskDistribution);
while ( my $client = $distClients->next() ) {

    # !!! Just in case...
    #
    next if $client->client_id == kCMClientID;
    next if $client->client_id == 4;             # BIG_FISH
    next if $client->client_id == 42;            # IDEA
    next if $client->client_id == 23;            # COMPASS
    next if $client->client_id == 38;            # BLIND_PIG
    next if $client->client_id == 9;             # LAUGH
    next if $client->client_id == 60;            # RSDEMO

    #	next if $client->client_id == 122; # CENTAUR
    next if $client->client_id == 123;           # WELK
    next if $client->client_id == 202;           # rstest
    next if $client->client_id == 288;           # rstest

    $gDistClientIDs{ $client->client_id } = 1;
}

# We're also going to need to know which ids get mapped.
# !!! We might want to put these into a table someplace?
# !!! But for now I'll just hard-code it.
#
my %gIDsToRemap = (
    track_id         => 1,
    album_id         => 1,
    product_id       => 1,
    master_id        => 1,
    song_id          => 1,
    artist_id        => 1,
    composer_id      => 1,
    label_id         => 1,
    asset_id         => 1,
    product_track_id => 1,
);

my $changes = Common::DB::Item::TableChangeLog->GetAllNew();
while ( my $change = $changes->next() ) {

    # Delete entries for non-distribution clients
    # !!! Not going to delete - going to mark these as 'skipped'.
    # !!! Seems safer until we get all the kinks worked out.
    #
    if ( !$gDistClientIDs{ $change->client_id } ) {
        $change->status(Common::DB::Item::TableChangeLog::kStatusSkipped);
        $change->save();
        next;
    }

    # !!! Wrap this whole damn thing in an eval block.
    # If something throwns an exeception, we'll mark the record and move on.
    #

    eval {
        my $tableDef = $gTableMap{ $change->table_name };
        if ( !$tableDef ) {

            # The log table is now catching changes for tables that this script may not care about.
            # That's not really an error.
            #
            $change->status(Common::DB::Item::TableChangeLog::kStatusSkipped);
            $change->save();
            next;
        }

        if ( Common::DB::Item::TableChangeLog::kActionDelete eq $change->action ) {
            handleDelete( $change, $tableDef );
        } elsif ( Common::DB::Item::TableChangeLog::kActionInsert eq $change->action ) {

            # !!! If we already _have_ this record in the CM data, then we want to treat
            # !!! this like an update...
            #
            handleInsert( $change, $tableDef );
        } elsif ( Common::DB::Item::TableChangeLog::kActionUpdate eq $change->action ) {
            handleUpdate( $change, $tableDef );
        } else {

            # We don't recognize this action for some reason.  It is probably a malformed
            # log entry.  So we'll mark it as bad rather than deleting it, so we have some
            # hope of later fixing the problem.
            #
            logError(
                "ERROR - Unknown action '" . $change->action . "' for log entry " . $change->table_change_log_id . " - marking as bad" );
            $change->status(Common::DB::Item::TableChangeLog::kStatusBad);
            $change->save();
        }
    };
    if ($@) {
        logError( "ERROR - Caught exception: $@ : for log entry " . $change->table_change_log_id . " - marking as bad" );
        $change->status(Common::DB::Item::TableChangeLog::kStatusBad);
        $change->save();
    }
}

if ( defined $gErrors ) {
    open( MAIL, "|/usr/lib/sendmail -oi -t" );
    print MAIL "From: consumeMetaDataChangeLog\n";
    print MAIL 'To: knott@royaltyshare.com' . "\n";
    print MAIL 'Subject: ERROR REPORT' . "\n\n";
    print MAIL "$gErrors\n";
    close(MAIL);
}

sub handleDelete {
    my ( $change, $tableDef ) = @_;

    my $whereHash = parsePairString( $change->where_pairs );
    remapClientIDs( $whereHash, $change->client_id );

    # Fetch the current metadata record.
    #
    my $class = $tableDef->{class};
    my $data  = $class->Lookup(%$whereHash);
    if ( !$data ) {
        logError( "ERROR ON DELETE - could not find matching record for log entry " . $change->table_change_log_id . " - marking as bad" );
        $change->status(Common::DB::Item::TableChangeLog::kStatusBad);
        $change->save();
        return;
    }

    # !!! We need some special handling for the artist and composer tables.
    # !!! Basically, we _never_ delete artist or composers.
    #
    if ( $tableDef->{noDelete} ) {
        $change->status(Common::DB::Item::TableChangeLog::kStatusNoDelete);
        $change->save();
        return;
    }

    # Delete it!
    #
    $data->delete();

    # Done with the change log item
    #
    $change->status(Common::DB::Item::TableChangeLog::kStatusProcessed);
    $change->save();

    #    $change->delete();
}

sub handleInsert {
    my ( $change, $tableDef ) = @_;

    my $setHash = parsePairString( $change->set_pairs );

    # Pull the auto-increment value out of the hash, if there is one.
    # !!! Well, currently every table we map has an auto-increment column...
    #
    # !!! If we already _have_ a record with this id, we want to treat this like an update.
    #
    my $autoFieldName = $tableDef->{autoField};
    my $autoFieldValue;
    if ( defined $autoFieldName ) {
        $autoFieldValue = $setHash->{$autoFieldName};
        if ( !defined $autoFieldValue ) {
            logError( "ERROR ON INSERT- missing primary key for log entry " . $change->table_change_log_id . " - marking as bad" );
            $change->status(Common::DB::Item::TableChangeLog::kStatusBad);
            $change->save();
            return;
        }

        # ... actually, if the record is already there, I think we want to _punt_.
        #
        my $mapping = Common::DB::Item::ClientCMIDMap->Lookup(
            client_id      => $change->client_id,
            id_type        => $autoFieldName,
            client_site_id => $autoFieldValue,
        );

        if ($mapping) {
            logError( "WARNING - Skipping INSERT for change " . $change->table_change_log_id . " - record with that key already exists" );
            $change->status(Common::DB::Item::TableChangeLog::kStatusSkipped);
            $change->save();
            return;
        }

        delete $setHash->{$autoFieldName};
    }

    # Now we re-map the ids.
    #
    remapClientIDs( $setHash, $change->client_id );

    my $class = $tableDef->{class};

    # If we're going to insert a record with a unique key, we need to see if a record
    # with that key already exists!
    # If it does, then we just want to re-use that id, and not try to create a new record.
    #
    if ( $tableDef->{uniqueKey} && defined $setHash->{ $tableDef->{uniqueKey} } ) {
        my $previousRecord = $class->Lookup( $tableDef->{uniqueKey} => $setHash->{ $tableDef->{uniqueKey} } );
        if ($previousRecord) {
            my $cmIDValue = $previousRecord->$autoFieldName;

            my $newIDMapping = Common::DB::Item::ClientCMIDMap->Create(
                client_id      => $change->client_id,
                client_site_id => $autoFieldValue,
                cm_site_id     => $cmIDValue,
                id_type        => $autoFieldName,
            );
            $newIDMapping->save();

            $change->status(Common::DB::Item::TableChangeLog::kStatusDuplicate);
            $change->save();
            return;
        }
    }

    # Create the new metadata record.
    #
    my $newItem = $class->Create(%$setHash);
    $newItem->save();

    # If there is an autoincrement primary key on this table, get the new id
    # and add that to the mapping table.
    #
    if ( defined $autoFieldName ) {
        my $cmIDValue = $newItem->$autoFieldName;

        my $newIDMapping = Common::DB::Item::ClientCMIDMap->Create(
            client_id      => $change->client_id,
            client_site_id => $autoFieldValue,
            cm_site_id     => $cmIDValue,
            id_type        => $autoFieldName,
        );
        $newIDMapping->save();
    }

    # All done, we can get rid of the change entry
    #
    #    $change->delete();
    $change->status(Common::DB::Item::TableChangeLog::kStatusProcessed);
    $change->save();
}

sub handleUpdate {
    my ( $change, $tableDef ) = @_;

    # Parse both pair strings into hashes.
    #
    my $setHash   = parsePairString( $change->set_pairs );
    my $whereHash = parsePairString( $change->where_pairs );

    # remap the ids.
    # Note!  All these ids should already exist in the table!
    #
    eval {
        remapClientIDs( $whereHash, $change->client_id );
        remapClientIDs( $setHash,   $change->client_id );
    };
    if ($@) {
        logError( "ERROR ON UPDATE - problem finding id map for log entry " . $change->table_change_log_id . " - $@ - marking as bad" );
        $change->status(Common::DB::Item::TableChangeLog::kStatusBad);
        $change->save();
        return;
    }

    # Fetch the current metadata record.
    #
    my $class = $tableDef->{class};
    my $data  = $class->Lookup(%$whereHash);
    if ( !$data ) {
        logError( "ERROR ON UPDATE - could not find matching record for log entry " . $change->table_change_log_id . " - marking as bad" );
        $change->status(Common::DB::Item::TableChangeLog::kStatusBad);
        $change->save();
        return;
    }

    # Now update the record, and save the changes.
    #
    # !!! Going to be paranoid, and do this in an eval, just in case we somehow
    # get a bogus field name and the DB::Item throws an exception.
    #
    eval {
        foreach my $key ( keys %$setHash ) {
            $data->$key( $setHash->{$key} );
        }

        $data->save();
    };
    if ($@) {
        logError( "ERROR ON UPDATE - id:" . $change->table_change_log_id . " threw this exception: $@ - marking as bad" );
        $change->status(Common::DB::Item::TableChangeLog::kStatusBad);
        $change->save();
        return;
    }

    # Done with the change log item.
    #
    #    $change->delete();
    $change->status(Common::DB::Item::TableChangeLog::kStatusProcessed);
    $change->save();
}

sub remapClientIDs {
    my ( $hash, $clientID ) = @_;

    my $assetID;

    foreach my $keyName ( keys %$hash ) {

        # Want to limit hitting the database to recognized id types
        #
        next unless $gIDsToRemap{$keyName};

        # Need to handle asset_id differently!
        # I will need to get the product_type_id from the product table.
        # This might be tricky.
        #
        if ( 'asset_id' eq $keyName ) {

            # Let's wait until the rest of the mapping is complete.
            # Then (in theory) we'll have a valid, re-mapped product_id.
            #
            $assetID = $hash->{$keyName};
            next;
        }

        my $mapping = Common::DB::Item::ClientCMIDMap->Lookup(
            client_id      => $clientID,
            id_type        => $keyName,
            client_site_id => $hash->{$keyName},
        );
        if ($mapping) {
            $hash->{$keyName} = $mapping->cm_site_id;
        } else {
            die "ERROR - unable to map $keyName : " . $hash->{$keyName};
        }
    }

    if ($assetID) {
        my $productTypeID = $hash->{product_type_id};
        if ( !$productTypeID ) {

            # Get the product id.
            # This really _must_ work, or we're hosed.
            #
            my $productID = $hash->{product_id};
            die "ERROR - attempting to remap asset_id, but cannot find product_id!" unless $productID;

            my $cmSiteProduct = RPS::DB::Item::Product->Lookup( product_id => $productID );
            die "ERROR - attempting to remap asset_id, but cannot find product with product_id $productID" unless $cmSiteProduct;
            $productTypeID = $cmSiteProduct->product_type_id;
        }

        my $assetType = 'album_id';
        if ( RPS::DB::Item::Product::kProductTypeDigitalTrack == $productTypeID ) {
            $assetType = 'track_id';
        }

        my $mapping = Common::DB::Item::ClientCMIDMap->Lookup(
            client_id      => $clientID,
            id_type        => $assetType,
            client_site_id => $assetID,
        );
        if ($mapping) {
            $hash->{'asset_id'} = $mapping->cm_site_id;
        }
    }
}

sub parsePairString {
    my ($string) = @_;

    my %resultHash;
    my @pairs = Text::ParseWords::quotewords( ',', 0, $string );
    foreach my $pair (@pairs) {
        my ( $key, $value ) = Text::ParseWords::quotewords( '=', 0, $pair );
        $resultHash{$key} = $value;
    }

    return \%resultHash;
}

sub logError {
    my ($error) = @_;

    Common::Log::Print($error);
    $gErrors .= $error . "\n";
}
