#!/usr/bin/perl -Ilibs -I../www/libs

use strict;
use warnings;

package API::PartialVehicleAdmin;

use Moose;
use Algorithm::Combinatorics;
use Bytes::Random::Secure;
use Carp;
use JSON::MaybeXS;
use List::MoreUtils;
use Storable;
use Try::Tiny;

use ECS::CDN::S3;
use ECS::Config;
use ECS::DB::Schema;
use ECS::LOOPS::VehicleNav;
use ECS::PartialVehicle;
use ECS::Vehicle;
use ECS::SiteSetting;

use API::Controller;
extends "API::Controller",
        "SOAP::Server::Parameters";

has '_navSteps' => (
    'is'      => "ro",
    'isa'     => "HashRef",
    'default' => sub {{
        'Audi'       => [ "Make", "Chassis", "Model", "Engine" ],
        'BMW'        => [ "Make", "Chassis", "Model", "Engine" ],
        'Volkswagen' => [ "Make", "Chassis", "Model", "Engine" ],
    }},
);



has '_json' => (
    'is' => "ro",
    'isa'     => "Object",
    'lazy'    => 1,
    'builder' => "_build__json",
);

__PACKAGE__->meta->make_immutable;

###############################################################################
# builders                                                                    #
###############################################################################

sub _build__json { JSON::MaybeXS->new( 'utf8' => 1, 'canonical' => 1 ) }

###############################################################################
# methods                                                                     #
###############################################################################

sub index {
    my $self = shift->new( @_ );

    my $makes = Storable::dclone( \%ECS::Vehicle::MAKES );
    for my $makeID ( keys %{ $makes } ) {
        my $make = $makes->{ $makeID };
        $make->{ 'makeID' } = $makeID;
        my $makeName = $make->{ 'makeName' };

        # important to make a copy of this and not modify the original
        my @navSteps = @{ $self->_navSteps->{ $makeName } || [] };
        if ( @navSteps ) {
            # remove "Make" step, it's being handled separately
            shift @navSteps;
            $make->{ 'navSteps' } = \@navSteps;
        }
    }
    $self->data->{ 'makes' } = $makes;

    $self->data->{ 'templates' } = [ "index.html" ];
    return $self->result;
}

###############################################################################

sub getEnabledCombinations {
    my $self = shift->new( @_ );

    my $makeID = $self->input->{ 'makeID' } or croak "Missing makeID";
    my $combinationsJSON = $self->input->{ 'combinationsJSON' }
        or croak "Missing combinationsJSON";

    my $combinations = try {
        $self->_json->decode( $combinationsJSON );
    }
    catch {
        croak "Invalid combinationsJSON: $_";
    };

    my @enabledCombinations = ();
    my $schema = $self->_getSchema( $makeID );
    for my $combination ( @{ $combinations } ) {
        my $filter;
        if ( $ECS::Vehicle::SHORT_MAKE_IDS{ $makeID } ) {
            $filter = $self->_getVehicleNavFilters( $makeID, $combination );
        }
        else {
            $filter = $self->_getVehicleConfigFilters( $makeID, $combination );
        }
        $filter->{ 'enabled' } = "YES";
        my $count = $schema->search( $filter )->count;
        push @enabledCombinations, ( $count > 0 );
    }

    $self->data->{ 'data' }->{ 'enabledCombinations' } = \@enabledCombinations;
    $self->data->{ 'dataType' } = "json";
    return $self->result;
}

###############################################################################

sub _getSchema {
    my ( $self, $makeID ) = @_;

    my $vehicleSchema = "Vehicle";
    $vehicleSchema = "PartialVehicle" if $ECS::Vehicle::SHORT_MAKE_IDS{ $makeID };

    return ECS::DB::Schema::getSchema( {
        'dbh'    => $self->dbh,
        'schema' => $vehicleSchema,
    } );
}

###############################################################################

sub getPartialVehicles {
    my $self = shift->new( @_ );

    my $makeID = $self->input->{ 'makeID' } or croak "Missing makeID";
    my $combinationJSON = $self->input->{ 'combinationJSON' }
        or croak "Missing combinationJSON";
    my $combination = try {
        $self->_json->decode( $combinationJSON );
    }
    catch {
        croak "Invalid combinationJSON: $_";
    };

    my $vehicles;
    if ( $ECS::Vehicle::SHORT_MAKE_IDS{ $makeID } ) {
        $vehicles = $self->_getVehicleNavVehicles( $makeID, $combination );
    }
    else {
        $vehicles = $self->_getVehicleConfigVehicles( $makeID, $combination );
    }

    # pack these objects with the Storage role
    # so that they can be serialized cleanly
    my @packedVehicles = map { $_->pack } @{ $vehicles };
    $self->data->{ 'data' }->{ 'partialVehicles' } = \@packedVehicles;

    $self->data->{ 'dataType' } = "json";
    return $self->result;
}

###############################################################################

sub _getVehicleNavVehicles {
    my ( $self, $makeID, $combination ) = @_;

    my $filters = $self->_getVehicleNavFilters( $makeID, $combination );
    my $vehicles = ECS::PartialVehicle::search( {
        'dbh'    => $self->dbhr,
        'search' => [
            $filters,
            { 'order_by' => "label" },
        ],
    } );

    return $vehicles;
}

###############################################################################

sub _getVehicleNavFilters {
    my ( $self, $makeID, $combination ) = @_;

    croak "Missing makeID" unless $makeID;

    my $make = $ECS::Vehicle::MAKES{ $makeID };
    my $makeName = $make->{ 'makeName' };
    my $navSteps = $self->_navSteps->{ $makeName };

    my %stepFilters = (
        'L1' => $makeName,
    );
    # translate the names (e.g. "Engine") into the nav steps (e.g. "L5")
    # starting from 1 instead of 0 to skip "Make" as it's handled separately
    for my $i ( 1 .. $#{ $navSteps } ) {
        my $stepName = $navSteps->[ $i ];
        my $stepSpecified = List::MoreUtils::any { $_ eq $stepName } @{ $combination };
        my $stepField = "L" . ( $i + 1 );
        # if the step was specified, it better not be null
        # otherwise, it better be null
        $stepFilters{ $stepField } = $stepSpecified
                                   ? { '!=' => undef }
                                   : undef;
    }

    return \%stepFilters;
}

###############################################################################

sub _getVehicleConfigVehicles {
    my ( $self, $makeID, $combination ) = @_;

    my $filters = $self->_getVehicleConfigFilters( $makeID, $combination );
    my $vehicles = ECS::Vehicle->search( {
        'dbh'    => $self->dbhr,
        'search' => [
            $filters,
            { 'order_by' => "vehicleLabel" },
        ],
    } );

    return $vehicles;
}

###############################################################################

sub _getVehicleConfigFilters {
    my ( $self, $makeID, $combination ) = @_;

    croak "Missing makeID" unless $makeID;

    my %filters = (
        'makeID'        => $makeID,
        'detailVehicle' => 0,
        'vehicleURL'    => { '!=' => undef },
    );
    for my $stepName ( @ECS::Vehicle::STEPS ) {
        next if $stepName eq "makeID"; # handling this separately

        my $stepSpecified = List::MoreUtils::any { "${_}ID" eq $stepName } @{ $combination };
        # if the step was specified, it better not be zero
        # otherwise, it better be zero
        $filters{ $stepName } = $stepSpecified
                              ? { '!=' => 0 }
                              : 0;
    }

    return \%filters;
}

###############################################################################

sub saveVehicles {
    my $self = shift->new( @_ );

    my $vehicleJSON = $self->input->{ 'vehicleJSON' };
    croak "Missing vehicleJSON" unless $vehicleJSON;

    my $makeID = $self->input->{ 'makeID' };
    croak "Missing makeID" unless $makeID;

    my $vehicles;
    try {
        $vehicles = $self->_json->decode( $vehicleJSON );
    }
    catch {
        croak "Invalid vehicleJSON: $_";
    };

    my $idCol = "vehicleConfigID";
    $idCol = "partialVehicleID" if $ECS::Vehicle::SHORT_MAKE_IDS{ $makeID };

    my @ids = keys %{ $vehicles };
    my $schema = $self->_getSchema( $makeID );
    my $rows = $schema->search(
        { $idCol => \@ids },
    );
    my @updatedRows = ();
    while ( my $row = $rows->next ) {
        my $id = $row->$idCol;
        my $changes = $vehicles->{ $id };
        $row->set_column( "enabled", $changes->{ 'enabled' } ? "YES" : "NO" );
        push @updatedRows, $row if $row->is_changed;
        $row->update;
    }

    for my $row ( @updatedRows ) {
        my $vehicle;
        if ( $ECS::Vehicle::SHORT_MAKE_IDS{ $makeID } ) {
            $vehicle = ECS::PartialVehicle->new( {
                'dbh' => $self->dbh,
                'row' => $row,
            } );
        }
        else {
            $vehicle = ECS::Vehicle->new( {
                'dbh' => $self->dbh,
                'row' => $row,
            } );
        }

        $self->_updateRelatedVehicles( $vehicle );
    }

    $self->data->{ 'data' }->{ 'success' } = 1;
    $self->data->{ 'dataType' } = "json";
    return $self->result;
}

###############################################################################

sub _updateRelatedVehicles {
    my ( $self, $vehicle ) = @_;

    my @unsortedDescendants = @{ $vehicle->descendantPartialVehicles };
    my @sortedDescendants = sort {
        $self->_cmpDescendants( $a, $b )
    } @unsortedDescendants;

    # for every vehicle underneath the one we just modified
    for my $descendantVehicle ( @sortedDescendants ) {
        # if every more generic version of that vehicle is enabled, enable this
        my $ancestorsEnabled = "YES";
        for my $ancestorVehicle ( @{ $descendantVehicle->ancestorPartialVehicles } ) {
            next if $ancestorVehicle->enabled eq "YES";

            # otherwise this shouldn't be enabled
            $ancestorsEnabled = "NO";
            last;
        }

        $descendantVehicle->row->set_column( "enabled", $ancestorsEnabled );
        $descendantVehicle->row->update;
    }

    return;
}

###############################################################################

sub _cmpDescendants {
    my ( $self, $a, $b ) = @_;

    my @aLevels = split /-/, $a->URL;
    my @bLevels = split /-/, $b->URL;

    return @aLevels <=> @bLevels;
}

###############################################################################

sub uploadImages {
    my $self = shift->new( @_ );

    my $makeID = $self->input->{ 'makeID' };
    croak "Missing makeID" unless $makeID;

    my $vehicleID = $self->input->{ 'vehicleID' };
    croak "Missing vehicleID" unless $vehicleID;

    $self->data->{ 'data' }->{ 'success' } = 1;
    $self->data->{ 'dataType' } = "json";

    my %images = ();
    for my $attachment ( @{ $self->parameters->parts } ) {
        my $name = $attachment->head->recommended_filename;
        next unless $name =~ m/^(small|medium|large)$/;
        my $content = $attachment->bodyhandle->as_string;
        $images{ $name } = $content;
    }

    # nothing to upload
    return $self->result unless %images;

    my $vehicle = $self->_getVehicleObject( $makeID, $vehicleID );
    croak "invalid vehicle" unless $vehicle;

    # need to set a filename for the images first
    for my $size ( keys %images ) {
        my $col = "${size}Image";
        my $name = Bytes::Random::Secure::random_bytes_hex( 16 );
        $vehicle->row->set_column( $col, $name );
    }

    # then upload using these filenames
    for my $size ( keys %images ) {
        my $content = $images{ $size };
        my $path = $vehicle->heroImg->{ $size };
        $self->_uploadImage( $path, $content );
    }
    $vehicle->row->update;

    return $self->result;
}

###############################################################################

sub _getVehicleObject {
    my ( $self, $makeID, $vehicleID ) = @_;

    if ( $ECS::Vehicle::SHORT_MAKE_IDS{ $makeID } ) {
        my $results = ECS::PartialVehicle::search( {
            'dbh'    => $self->dbh,
            'search' => [
                { 'partialVehicleID' => $vehicleID },
            ],
        } );
        return $results->[ 0 ];
    }

    my $results = ECS::Vehicle->search( {
        'dbh'    => $self->dbh,
        'search' => [
            { 'vehicleConfigID' => $vehicleID },
        ],
    } );
    return $results->[ 0 ];
}

###############################################################################

sub _uploadImage {
    my ($self, $name, $content) = @_;

    croak "Missing name" unless $name;
    croak "Missing content" unless $content;

    my $s3 = ECS::CDN::S3->new();
    my $object_name = $s3->build_path('', $name); #since name already holds the path first paraemter will be empty
  
    my $metadata = {
        'Content-Type'        => 'image/jpeg',
        'Content-Disposition' => 'inline',
    };

    $s3->post($object_name, $content, $metadata);

    return;
}


###############################################################################

sub getLinkLimits {
    my $self = shift->new( @_ );

    my $schema = ECS::DB::Schema::getSchema( {
        'dbh'    => $self->dbh,
        'schema' => "PartialVehicleLinkLimits",
    } );

    my @results = $schema->search->all;
    my @linkLimits = map $_->TO_JSON, @results;

    $self->data->{ 'data' }->{ 'linkLimits' } = \@linkLimits;
    $self->data->{ 'dataType' } = "json";
    return $self->result;
}

###############################################################################

sub getContent {
    my ( $self ) = shift->new( @_ );

    my $partialVehicleID = $self->input->{ 'partialVehicleID' }
        or croak "Missing partialVehicleID";

    my $vehicles = ECS::PartialVehicle::search( {
        'dbh'    => $self->dbhr,
        'search' => [ { 'partialVehicleID' => $partialVehicleID } ],
    } );
    croak "vehicle not found" unless @{ $vehicles };
    my $vehicle = $vehicles->[ 0 ];

    $self->data->{ 'data' }->{ 'description' } = $vehicle->description;
    $self->data->{ 'data' }->{ 'feature'     } = $vehicle->feature;
    $self->data->{ 'dataType' } = "json";
    return $self->result;
}

###############################################################################

sub saveContent {
    my ( $self ) = shift->new( @_ );

    my $partialVehicleID = $self->input->{ 'partialVehicleID' }
        or croak "Missing partialVehicleID";
    my $description = $self->input->{ 'description' }; # optional
    my $feature     = $self->input->{ 'feature' };     # optional

    my $vehicles = ECS::PartialVehicle::search( {
        'dbh'    => $self->dbh,
        'search' => [ { 'partialVehicleID' => $partialVehicleID } ],
    } );
    croak "vehicle not found" unless @{ $vehicles };
    my $vehicle = $vehicles->[ 0 ];

    $vehicle->row->set_column( "description", $description );
    $vehicle->row->set_column( "feature",     $feature     );
    $vehicle->row->update;

    $self->data->{ 'data' }->{ 'success' } = 1;
    $self->data->{ 'dataType' } = "json";
    return $self->result;
}

###############################################################################

sub saveLinkLimits {
    my $self = shift->new( @_ );

    my $schema = ECS::DB::Schema::getSchema( {
        'dbh'    => $self->dbh,
        'schema' => "PartialVehicleLinkLimits",
    } );
    my $rows = $schema->search(
        { "limitID" => "1" },
    );
    my %limits = %{ $self->input };
    
    # verify the values are positive integers
    foreach my $key ( keys %limits ) {
        next if $key eq 'action';
        if ( not ( $limits{ $key } and $limits{ $key } =~ /^\d+$/ ) or $limits{ $key } < 0 ) {
            return $self->error;
        }
    }
    
    # set the new values
    my $row = $rows->next;
    foreach my $key ( keys %limits ) {
        next if $key eq 'action';
        $row->set_column($key, $limits{ $key });
        $row->update;
    }

    $self->data->{ 'data' }->{ 'success' } = 1;
    $self->data->{ 'dataType' } = "json";
    return $self->result;
}

1;
