package App::CISetup::Travis::ConfigFile;

use strict;
use warnings;
use namespace::autoclean;
use autodie qw( :all );

our $VERSION = '0.19';

use App::CISetup::Types qw( Bool File Str );
use File::pushd;
use File::Which qw( which );
use IPC::Run3 qw( run3 );
use List::AllUtils qw( first first_index uniq );
use Path::Iterator::Rule;
use Try::Tiny;
use YAML qw( Dump );

use Moose;
use MooseX::StrictConstructor;

has email_address => (
    is        => 'ro',
    isa       => Str,                   # todo, better type
    predicate => 'has_email_address',
);

has force_threaded_perls => (
    is      => 'ro',
    isa     => Bool,
    default => 0,
);

has perl_caching => (
    is      => 'ro',
    isa     => Bool,
    default => 1,
);

has github_user => (
    is        => 'ro',
    isa       => Str,
    predicate => 'has_github_user',
);

has slack_key => (
    is        => 'ro',
    isa       => Str,
    predicate => 'has_slack_key',
);

with 'App::CISetup::Role::ConfigFile';

## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
sub _create_config {
    my $self = shift;

    return $self->_update_config( { language => 'perl' }, 1 );
}

sub _update_config {
    my $self   = shift;
    my $travis = shift;
    my $create = shift;

    $self->_maybe_update_travis_perl_usage( $travis, $create );
    $self->_maybe_remove_sudo($travis);
    $self->_update_packages($travis);
    $self->_update_coverity_email($travis);
    $self->_update_notifications($travis);

    return $travis;
}
## use critic

sub _maybe_update_travis_perl_usage {
    my $self   = shift;
    my $travis = shift;
    my $create = shift;

    return
        unless $create
        || ( $travis->{before_install}
        && grep {/perl-travis-helper|travis-perl/}
        @{ $travis->{before_install} } );

    $self->_maybe_add_cache_block($travis);
    $self->_fixup_helpers_usage($travis);
    $self->_rewrite_perl_block($travis);
    $self->_update_perl_matrix($travis);
    $self->_update_env_vars($travis);

    return;
}

sub _maybe_add_cache_block {
    my $self   = shift;
    my $travis = shift;

    return unless $self->perl_caching;
    return if exists $travis->{cache};

    $travis->{cache} = { directories => ['$HOME/perl5'] };

    return;
}

sub _fixup_helpers_usage {
    my $self   = shift;
    my $travis = shift;

    if (
        ( @{ $travis->{script} // [] } && @{ $travis->{script} } > 3 )
        || (
            $travis->{install}
            && ( grep { !/cpan-install/ } @{ $travis->{install} }
                || @{ $travis->{install} } > 2 )
        )
    ) {

        my $i = (
            first_index {/travis-perl|haarg/}
            @{ $travis->{before_install} }
        ) // 0;
        $travis->{before_install}->[$i]
            = 'git clone git://github.com/travis-perl/helpers ~/travis-perl-helpers';
        $travis->{before_install}->[ $i + 1 ]
            = 'source ~/travis-perl-helpers/init';
    }
    else {
        delete $travis->{install};
        delete $travis->{script};

        $travis->{before_install} //= [];
        my $i = first_index {/travis-perl|haarg/}
        @{ $travis->{before_install} };
        $i = 0 if $i < 0;

        my $auto = 'eval $(curl https://travis-perl.github.io/init) --auto';
        $auto .= ' --always-upgrade-modules' if $self->perl_caching;

        $travis->{before_install}[$i] = $auto;
        splice( @{ $travis->{before_install} }, $i + 1, 0 )
            if @{ $travis->{before_install} } > 1;
    }

    return;
}

my @Perls = qw(
    blead
    dev
    5.30
    5.28
    5.26
    5.24
    5.22
    5.20
    5.18
    5.16
    5.14
    5.12
    5.10
    5.8
);

# XXX - if a build is intentionally excluding Perls besides 5.8 this will add
# those Perls back. Not sure how best to deal with this. We want to test on
# all Perls for most modules, and any manually generated file might forget to
# include some of them.
sub _rewrite_perl_block {
    my $self   = shift;
    my $travis = shift;

    my @perls = @Perls;
    for my $perl (qw( 5.8 5.10 5.12 )) {
        pop @perls
            unless grep {/\Q$perl/} @{ $travis->{perl} };
    }

    my $has_xs
        = defined Path::Iterator::Rule->new->file->name(qr/\.xs/)
        ->iter( $self->file->parent )->();

    if ( $self->force_threaded_perls || $has_xs ) {
        $travis->{perl} = [ map { ( $_, $_ . '-thr' ) } @perls ];
    }
    else {
        $travis->{perl} = \@perls;
    }

    return;
}

sub _update_perl_matrix {
    my $self   = shift;
    my $travis = shift;

    my @bleads = 'blead';
    push @bleads, 'blead-thr'
        if grep { $_ eq 'blead-thr' } @{ $travis->{perl} };

    my $latest  = first {/^5/} @Perls;
    my @include = @{ $travis->{matrix}{include} // [] };
    push @include, {
        perl => $latest,
        env  => 'COVERAGE=1',
        }
        unless grep { $_->{perl} eq $latest && $_->{env} eq 'COVERAGE=1' }
        @include;

    my @allow_failures = @{ $travis->{matrix}{allow_failures} // [] };
    for my $blead (@bleads) {
        push @allow_failures, { perl => $blead }
            unless grep { exists $_->{perl} && $_->{perl} eq $blead }
            @allow_failures;
    }

    $travis->{matrix} = {
        fast_finish    => 1,
        include        => \@include,
        allow_failures => \@allow_failures,
    };

    return;
}

sub _update_env_vars {
    my $self   = shift;
    my $travis = shift;

    $travis->{env} //= {};
    $travis->{env}{global} = [
        uniq(
            sort @{ $travis->{env}{global} // [] },
            qw(
                RELEASE_TESTING=1
                AUTHOR_TESTING=1
                ),
        )
    ];

    return;
}

sub _maybe_remove_sudo {
    my $self   = shift;
    my $travis = shift;

    delete $travis->{sudo};

    return;
}

sub _update_packages {
    my $self   = shift;
    my $travis = shift;

    my @addons
        = $travis->{addons}
        && $travis->{addons}{apt} && $travis->{addons}{apt}{packages}
        ? @{ $travis->{addons}{apt}{packages} }
        : ();
    push @addons, qw( aspell aspell-en )
        if $travis->{perl};
    $travis->{addons}{apt}{packages} = [ sort { $a cmp $b } uniq(@addons) ]
        if @addons;

    return;
}

sub _update_coverity_email {
    my $self   = shift;
    my $travis = shift;

    return unless $self->has_email_address;
    return unless $travis->{addons} && $travis->{addons}{coverity_scan};
    $travis->{addons}{coverity_scan}{notification_email}
        = $self->email_address;
}

sub _update_notifications {
    my $self   = shift;
    my $travis = shift;

    if ( $self->has_email_address ) {
        $travis->{notifications}{email} = {
            recipients => [ $self->email_address ],
            on_success => 'change',
            on_failure => 'always',
        };
    }

    if ( $self->has_slack_key && $self->has_github_user ) {
        my $slack = $travis->{notifications}{slack}{rooms}{secure};

        # travis encrypt will make a new encrypted version every time it's given
        # the same input so we don't want to run it unless we have to, otherwise
        # we end up with pointless updates.
        unless ($slack) {
            my $pushed = pushd( $self->file->parent );
            my $stdout;
            my $stderr;

            my $exe = which('travis')
                or die 'Cannot find a travis command in the PATH';
            $self->_run3(
                [
                    $exe, 'encrypt', '--no-interactive',
                    '-R',
                    $self->github_user . '/' . $self->file->parent->basename,
                    $self->slack_key
                ],
                \undef,
                \$stdout,
                \$stderr,
            );
            die $stderr if $stderr;
            $slack = $stdout =~ s/^\"|\"$//gr;
        }

        $travis->{notifications}{slack} = {
            rooms => { secure => $slack },
        };
    }

    return;
}

# This is broken out so we can replace it in test code.
sub _run3 {
    shift;
    run3(@_);
    return;
}

my @BlocksOrder = qw(
    sudo
    dist
    addons
    language
    compiler
    go
    jdk
    perl
    php
    python
    cache
    solution
    matrix
    fast_finish
    env
    branches
    services
    before_install
    install
    before_script
    script
    after_script
    after_success
    after_failure
    notifications
);

my %KnownBlocks = map { $_ => 1 } @BlocksOrder;

## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
sub _fix_up_yaml {
    my $self = shift;
    my $yaml = shift;

    $yaml =~ s/sudo: 0/sudo: false/g;

    return $self->_reorder_yaml_blocks( $yaml, \@BlocksOrder );
}

sub _reorder_addons_block {
    my $self  = shift;
    my $block = shift;

    return $block unless $block =~ /coverity_scan:\n(.+)(?=\S|\z)/ms;

    my %chunks;
    for my $line ( split /\n/, $1 ) {
        my ($name) = $line =~ / +([^:]+):/;
        $chunks{$name} = $line;
    }

    my $reordered = join q{}, map {"$chunks{$_}\n"}
        grep { $chunks{$_} }
        qw(
        project
        description
        name
        notification_email
        build_command_prepend
        build_command
        branch_pattern
    );

    return $block
        =~ s/coverity_scan:\n.+(?=\S|\z)/coverity_scan:\n$reordered/msr;
}

sub _cisetup_flags {
    my $self = shift;

    my %flags = (
        force_threaded_perls => $self->force_threaded_perls ? 1 : 0,
        perl_caching         => $self->perl_caching         ? 1 : 0,
    );

    $flags{email_address} = $self->email_address
        if $self->has_email_address;
    $flags{github_user} = $self->github_user
        if $self->has_github_user;

    return \%flags;
}
## use critic

__PACKAGE__->meta->make_immutable;

1;