# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

package Bugzilla::Extension::TryAutoLand;

use strict;

use base qw(Bugzilla::Extension);

use Bugzilla::Bug;
use Bugzilla::Attachment;
use Bugzilla::User;
use Bugzilla::Util qw(trick_taint diff_arrays);
use Bugzilla::Error;

use Bugzilla::Extension::TryAutoLand::Constants;

our $VERSION = '0.01';

BEGIN {
    *Bugzilla::Bug::autoland_branches             = \&_autoland_branches;
    *Bugzilla::Bug::autoland_try_syntax           = \&_autoland_try_syntax;
    *Bugzilla::Attachment::autoland_checked       = \&_autoland_attachment_checked;
    *Bugzilla::Attachment::autoland_who           = \&_autoland_attachment_who;
    *Bugzilla::Attachment::autoland_status        = \&_autoland_attachment_status;
    *Bugzilla::Attachment::autoland_status_when   = \&_autoland_attachment_status_when;
    *Bugzilla::Attachment::autoland_update_status = \&_autoland_attachment_update_status;
    *Bugzilla::Attachment::autoland_remove        = \&_autoland_attachment_remove;
}

sub db_schema_abstract_schema {
    my ($self, $args) = @_;
    $args->{'schema'}->{'autoland_branches'} = {
        FIELDS => [
            bug_id => {
                TYPE       => 'INT3',
                NOTNULL    => 1,
                PRIMARYKEY => 1,
                REFERENCES => {
                    TABLE  => 'bugs',
                    COLUMN => 'bug_id',
                    DELETE => 'CASCADE'
                }
            },
            branches => {
                TYPE    => 'VARCHAR(255)',
                NOTNULL => 1
            }, 
            try_syntax => {
                TYPE    => 'VARCHAR(255)', 
                NOTNULL => 1, 
                DEFAULT => "''", 
            }
        ],
    };

    $args->{'schema'}->{'autoland_attachments'} = {
        FIELDS => [
            attach_id => {
                TYPE       => 'INT3',
                NOTNULL    => 1,
                PRIMARYKEY => 1, 
                REFERENCES => {
                    TABLE  => 'attachments',
                    COLUMN => 'attach_id',
                    DELETE => 'CASCADE'
                },
            },
            who => {
                TYPE       => 'INT3',
                NOTNULL    => 1,
                REFERENCES => {
                    TABLE  => 'profiles',
                    COLUMN => 'userid',
                },
            },
            status => {
                TYPE    => 'varchar(64)',
                NOTNULL => 1
            }, 
            status_when => {
                TYPE    => 'DATETIME',
                NOTNULL => 1,
            },
        ],
    };
}

sub install_update_db {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;

    if (!$dbh->bz_column_info('autoland_branches', 'try_syntax')) {
        $dbh->bz_add_column('autoland_branches', 'try_syntax', {
            TYPE    => 'VARCHAR(255)',
            NOTNULL => 1, 
            DEFAULT => "''", 
        });
    }
}

sub _autoland_branches {
    my $self = shift;
    return $self->{'autoland_branches'} if exists $self->{'autoland_branches'};
    _preload_bug_data($self);
    return $self->{'autoland_branches'};
}

sub _autoland_try_syntax {
    my $self = shift;
    return $self->{'autoland_try_syntax'} if exists $self->{'autoland_try_syntax'};
    _preload_bug_data($self);
    return $self->{'autoland_try_syntax'};
}

sub _preload_bug_data {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    my $result = $dbh->selectrow_hashref("SELECT branches, try_syntax FROM autoland_branches 
                                           WHERE bug_id = ?", { Slice => {} }, $self->id);
    if ($result) {
        $self->{'autoland_branches'}   = $result->{'branches'};
        $self->{'autoland_try_syntax'} = $result->{'try_syntax'};
    }
    else {
        $self->{'autoland_branches'}   = undef;
        $self->{'autoland_try_syntax'} = undef;
    }
}

sub _autoland_attachment_checked {
    my $self = shift;
    my $dbh = Bugzilla->dbh;
    return $self->{'autoland_checked'} if exists $self->{'autoland_checked'};
    my $result = $dbh->selectrow_hashref("SELECT who, status, status_when 
                                            FROM autoland_attachments
                                           WHERE attach_id = ?", { Slice => {} }, $self->id);
    if ($result) {
        $self->{'autoland_checked'}     = 1;
        $self->{'autoland_who'}         = Bugzilla::User->new($result->{'who'});
        $self->{'autoland_status'}      = $result->{'status'};
        $self->{'autoland_status_when'} = $result->{'status_when'};
    }
    else {
        $self->{'autoland_checked'}     = 0;
        $self->{'autoland_who'}         = undef;
        $self->{'autoland_status'}      = undef;
        $self->{'autoland_status_when'} = undef;
    }
    return $self->{'autoland_checked'};
}

sub _autoland_attachment_who {
    my $self = shift;
    return undef if !$self->autoland_checked;
    return $self->{'autoland_who'};
}

sub _autoland_attachment_status {
    my $self = shift;
    return undef if !$self->autoland_checked;
    return $self->{'autoland_status'};
}

sub _autoland_attachment_status_when {
    my $self = shift;
    return undef if !$self->autoland_checked;
    return $self->{'autoland_status_when'};
}

sub _autoland_attachment_update_status {
    my ($self, $status) = @_;
    my $dbh = Bugzilla->dbh;

    return undef if !$self->autoland_checked;

    grep($_ eq $status, VALID_STATUSES)
        || ThrowUserError('autoland_invalid_status',
                          { status => $status, 
                            valid  => [ VALID_STATUSES ] });

    if ($self->autoland_status ne $status) {
        my $timestamp = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
        trick_taint($status);
        $dbh->do("UPDATE autoland_attachments SET status = ?, status_when = ?
                  WHERE attach_id = ?", undef, $status, $timestamp, $self->id);
        $self->{'autoland_status'}      = $status;
        $self->{'autoland_status_when'} = $timestamp;
    }

    return 1;
}

sub _autoland_attachment_remove {
    my ($self) = @_;
    my $dbh = Bugzilla->dbh;
    return undef if !$self->autoland_checked;
    $dbh->do("DELETE FROM autoland_attachments WHERE attach_id = ?", undef, $self->id);
    delete $self->{'autoland_checked'};
    delete $self->{'autoland_who'};
    delete $self->{'autoland_status'};
    delete $self->{'autoland_status_when'};
}

sub object_end_of_update {
    my ($self, $args) = @_;
    my $object = $args->{'object'};
    my $user   = Bugzilla->user;
    my $dbh    = Bugzilla->dbh;
    my $cgi    = Bugzilla->cgi;
    my $params = Bugzilla->input_params;

    return if !$user->in_group('autoland');

    if ($object->isa('Bugzilla::Bug')) {
        # First make any needed changes to the branches and try_syntax fields
        my $bug_id = $object->bug_id;
        my $bug_result = $dbh->selectrow_hashref("SELECT branches, try_syntax 
                                                    FROM autoland_branches 
                                                  WHERE bug_id = ?", 
                                                 { Slice => {} }, $bug_id);

        my $old_branches   = '';
        my $old_try_syntax = '';
        if ($bug_result) {
            $old_branches   = $bug_result->{'branches'};
            $old_try_syntax = $bug_result->{'try_syntax'};
        }

        my $new_branches   = $params->{'autoland_branches'} || '';
        my $new_try_syntax = $params->{'autoland_try_syntax'} || '';

        my $set_attachments = [];
        if (ref $params->{'autoland_attachments'}) {
            $set_attachments = $params->{'autoland_attachments'};
        } elsif ($params->{'autoland_attachments'}) {
            $set_attachments = [ $params->{'autoland_attachments'} ];
        }

        # Check for required values
        (!$new_branches && @{$set_attachments}) 
            && ThrowUserError('autoland_empty_branches');     
        ($new_branches && !$new_try_syntax) 
            && ThrowUserError('autoland_empty_try_syntax');

        trick_taint($new_branches);
        if (!$new_branches && $old_branches) {
            $dbh->do("DELETE FROM autoland_branches WHERE bug_id = ?",
                     undef, $bug_id);
        }
        elsif ($new_branches && !$old_branches) {
            $dbh->do("INSERT INTO autoland_branches (bug_id, branches)
                      VALUES (?, ?)", undef, $bug_id, $new_branches); 
        }
        elsif ($old_branches ne $new_branches) {
            $dbh->do("UPDATE autoland_branches SET branches = ? WHERE bug_id = ?",
                     undef, $new_branches, $bug_id);
        }

        trick_taint($new_try_syntax);
        if (($old_try_syntax ne $new_try_syntax) && $new_branches) {
            $dbh->do("UPDATE autoland_branches SET try_syntax = ? WHERE bug_id = ?",
                     undef, $new_try_syntax, $bug_id);
        }

        # Next make any changes needed to each of the attachments.
        # 1. If an attachment is checked it has a row in the table, if
        # there is no row in the table it is not checked.
        # 2. Do not allow changes to checked state if status == 'running' or status == 'waiting'
        my $check_attachments = ref $params->{'defined_autoland_attachments'}
                                ? $params->{'defined_autoland_attachments'}
                                : [ $params->{'defined_autoland_attachments'} ];
        my ($removed_attachments) = diff_arrays($check_attachments, $set_attachments);
        foreach my $attachment (@{$object->attachments}) {
            next if !$attachment->ispatch;
            my $attach_id = $attachment->id;

            my $checked = (grep $_ == $attach_id, @$set_attachments) ? 1 : 0;
            my $unchecked = (grep $_ == $attach_id, @$removed_attachments) ? 1 : 0;
            my $old_checked = $dbh->selectrow_array("SELECT 1 FROM autoland_attachments
                                                     WHERE attach_id = ?", undef, $attach_id) || 0;

            next if $checked && $old_checked;

            if ($unchecked && $old_checked && $attachment->autoland_status =~ /^(failed|success)$/) {
                $dbh->do("DELETE FROM autoland_attachments WHERE attach_id = ?", undef, $attach_id);
            }
            elsif ($checked && !$old_checked) {
                $dbh->do("INSERT INTO autoland_attachments (attach_id, who, status, status_when) 
                          VALUES (?, ?, 'waiting', now())", undef, $attach_id, $user->id);
            }
        }

    }
}

sub template_before_process {
    my ($self, $args) = @_;
    my $file = $args->{'file'};
    my $vars = $args->{'vars'};

    # in the header we just need to set the var to ensure the css gets included
    if ($file eq 'bug/show-header.html.tmpl' && Bugzilla->user->in_group('autoland') ) {
        $vars->{'autoland'} = 1;
    }

    if ($file eq 'bug/edit.html.tmpl') {
        $vars->{'autoland_default_try_syntax'} = DEFAULT_TRY_SYNTAX;
    }
}

sub webservice {
    my ($self, $args) = @_;

    my $dispatch = $args->{dispatch};
    $dispatch->{TryAutoLand} = "Bugzilla::Extension::TryAutoLand::WebService";
}

__PACKAGE__->NAME;