package Cpanel::Easy::Utils::StateRef;

# cpanel - Cpanel/Easy/Utils/StateRef.pm          Copyright(c) 2014 cPanel, Inc.
#                                                           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 File::Spec ();
no warnings qw(redefine);

sub generate_state_hashref {
    my ( $self, $namespace ) = @_;

    $namespace = ref $self if !$self->is_namespace($namespace);

    # NOTE: This namespace is always Cpanel::Easy, which will always exists.
    # So, there's not really any sane reason to check return value of
    # get_file_from_namespace()
    my $file = $self->get_file_from_namespace($namespace);
    my $dir  = $file;
    $dir =~ s{\.pm$}{};

    my $setup = { 'started' => time, };

    # 1) proc $namespace vers
    foreach my $ver ( @{ $self->{'state_config'}{'main_vers'} } ) {
        my $temp = {};
        my $curn = $namespace . '::' . $ver;
        $self->include_ns_into_hashref( $curn, $temp );
        $setup->{'optmods'}{ $self->{'state_config'}{'main_name'} }{'_main'}{$ver} = $temp->{'optmods'}{$curn};

        # support $namespace hr? Y-> merge with $namespace's
    }

    my @ignore = ref $self->{'state_config'}{'ignore'} eq 'ARRAY' ? @{ $self->{'state_config'}{'ignore'} } : ();
    my %skip;
    foreach my $ignore (@ignore) {
        $skip{$ignore} = [];
        $skip{ $ignore . '.pm' } = [];
    }
    foreach my $ignore (qw( . .. Utils Utils.pm UI UI.pm )) {
        $skip{$ignore} = [];
    }

    # 2) proc all in $INC{ $file }'s $dir except %skip
    $self->include_dir_into_hashref( $namespace, $dir, $setup, \%skip );

    # 3) $self->{'state_config'}{'include'}
    if ( ref $self->{'state_config'}{'include'} eq 'ARRAY' ) {
        for my $inc_ns ( @{ $self->{'state_config'}{'include'} } ) {
            $self->include_ns_into_hashref( $inc_ns, $setup, 1 );
            my $dir = $self->get_dir_from_namespace($inc_ns);
            next unless $dir;    # skip invalid namespaces (Case 90457)
            $self->include_dir_into_hashref( $inc_ns, $dir, $setup, \%skip, 1 );
        }
    }

    #4) include custom ones:
    my $idx = 0;                 # first one is not an include, after that they are:
    for my $ns ( $namespace, 'Cpanel::Easy', @{ $self->{'state_config'}{'include'} } ) {
        my $dir = $self->get_dir_from_namespace( $ns, 1 );
        next unless $dir;        # skip invalid namespaces (Case 90457)
        my $path = File::Spec->catdir( $self->{'opt_mod_custom_dir'}, $dir );
        $self->include_dir_into_hashref( $ns, $path, $setup, \%skip, $idx );
        $idx++;
    }

    $self->set_state_tarballs_key($setup);
    $self->set_state_ensurepkgs_key($setup);
    $self->set_state_order_key($setup);

    # $self->{'easyconfig_hr_fetch_errors'} has error
    # loading variations, DO NOT pass to add_error_detail() - case 844

    delete $setup->{'optmods'}{'_main'};

    $setup->{'created'} = time;

    return $setup;
}

sub set_state_tarballs_key {
    my ( $self, $state_hr ) = @_;

    $state_hr->{'tarballs'} = {};

    my $main_name = $self->{'state_config'}{'main_name'};

    for my $vers ( @{ $self->{'state_config'}{'main_vers'} } ) {

        my $ons  = 'Cpanel::Easy::' . $main_name . '::' . $vers;
        my $file = $self->get_file_from_namespace($ons);
        next unless $file;    # skip invalid namespaces (Case 90457)
        my $ptch = $file . '.patch.tar.gz';
        $file .= '.tar.gz';
        $state_hr->{'tarballs'}{$ons} = [];

        push @{ $state_hr->{'tarballs'}{$ons} }, $file
          if -e $file || $state_hr->{'optmods'}{$main_name}{'_main'}{$vers}{'hastargz'};

        push @{ $state_hr->{'tarballs'}{$ons} }, $ptch
          if -e $ptch || $state_hr->{'optmods'}{$main_name}{'_main'}{$vers}{'haspatch'};

        delete $state_hr->{'tarballs'}{$ons} if !@{ $state_hr->{'tarballs'}{$ons} };
    }

    for my $pns ( keys %{ $state_hr->{'optmods'}{$main_name} } ) {

        # these shoudln't be here
        if ( !$self->is_namespace($pns) ) {
            delete $state_hr->{'optmods'}{$main_name}{$pns};
            next;
        }

        next if $pns eq '_main';

        my $ons  = 'Cpanel::Easy::' . $main_name . '::' . $pns;
        my $file = $self->get_file_from_namespace($ons);
        next unless $file;    # skip invalid namespaces (Case 90457)
        my $ptch = $file . '.patch.tar.gz';
        $file .= '.tar.gz';
        $state_hr->{'tarballs'}{$ons} = [];

        push @{ $state_hr->{'tarballs'}{$ons} }, $file
          if -e $file || $state_hr->{'optmods'}{$main_name}{$pns}{'hastargz'};

        push @{ $state_hr->{'tarballs'}{$ons} }, $ptch
          if -e $ptch || $state_hr->{'optmods'}{$main_name}{$pns}{'haspatch'};

        delete $state_hr->{'tarballs'}{$ons} if !@{ $state_hr->{'tarballs'}{$ons} };
    }

    for my $ns ( keys %{ $state_hr->{'optmods'} } ) {
        next if $ns eq $main_name;
        next if $ns eq '_meta';
        next if !$self->is_namespace($ns);

        my $file = $self->get_file_from_namespace($ns);
        next unless $file;    # skip invalid namespaces (Case 90457)
        my $ptch = $file . '.patch.tar.gz';
        $file .= '.tar.gz';
        $state_hr->{'tarballs'}{$ns} = [];

        push @{ $state_hr->{'tarballs'}{$ns} }, $file
          if -e $file || $state_hr->{'optmods'}{$ns}{'hastargz'};

        push @{ $state_hr->{'tarballs'}{$ns} }, $ptch
          if -e $ptch || $state_hr->{'optmods'}{$ns}{'haspatch'};

        delete $state_hr->{'tarballs'}{$ns} if !@{ $state_hr->{'tarballs'}{$ns} };
    }
}

sub set_state_ensurepkgs_key {
    my ( $self, $state_hr ) = @_;

    $state_hr->{'ensurepkgs'} = {};

    my $main_name = $self->{'state_config'}{'main_name'};

    for my $vers ( @{ $self->{'state_config'}{'main_vers'} } ) {
        my $ons = 'Cpanel::Easy::' . $main_name . '::' . $vers;

        if ( exists $state_hr->{'optmods'}{'_main'}{$vers}{'ensurepkg'} ) {
            $state_hr->{'ensurepkgs'}{$ons} = $state_hr->{'optmods'}{'_main'}{$vers}{'ensurepkg'};
        }
    }

    for my $pns ( keys %{ $state_hr->{'optmods'}{$main_name} } ) {
        next if $pns eq '_main';
        my $ons = 'Cpanel::Easy::' . $main_name . '::' . $pns;

        if ( exists $state_hr->{'optmods'}{$main_name}{$pns}{'ensurepkg'} ) {
            $state_hr->{'ensurepkgs'}{$ons} = $state_hr->{'optmods'}{$main_name}{$pns}{'ensurepkg'};
        }
    }

    for my $ns ( keys %{ $state_hr->{'optmods'} } ) {
        next if $ns eq $main_name;
        next if $ns eq '_main';

        if ( exists $state_hr->{'optmods'}{$ns}{'ensurepkg'} ) {
            $state_hr->{'ensurepkgs'}{$ns} = $state_hr->{'optmods'}{$ns}{'ensurepkg'};
        }
    }
}

sub set_state_order_key {
    my ( $self, $state_hr ) = @_;
    $state_hr->{'order'} = [];

    my $nss = $self->get_state_ns_to_easyconfig_hashref($state_hr);

    my $main_name = $self->{'state_config'}{'main_name'};
    my $main      = 'Cpanel::Easy::' . $main_name;

    my @main_opts;
    for my $short ( keys %{ $state_hr->{'optmods'}{$main_name} } ) {
        next if $short eq '_main';
        push @main_opts, $main . '::' . $short;
    }

    for my $mv ( @{ $self->{'state_config'}{'main_vers'} } ) {
        my $main_vers = $main . '::' . $mv;

        for my $inc ( @{ $self->{'state_config'}{'include'} } ) {
            $nss->{$inc}{'depends'}{'optmods'}{$main_vers} = 1;
        }

        for my $opt (@main_opts) {
            $nss->{$main_vers}{'depends'}{'optmods'}{$opt} = 1;
        }
    }

    for my $inc ( @{ $self->{'state_config'}{'include'} } ) {
        eval "use $inc;";
        my @vers;

        my %ver_lookup;
        if ( $inc->can('versions') ) {
            for my $v ( $inc->versions() ) {
                $ver_lookup{$v} = 1;
            }
        }

        # add all nonversion sub opts to $inc :: ver
        if ( keys %ver_lookup ) {
            for my $item ( keys %{$nss} ) {
                if ( $item =~ m{ \A $inc\:\:(.*) \z }xms ) {
                    my $top = $1;
                    next if exists $ver_lookup{$top};
                    $nss->{$item}{'depends'}{'optmods'}{$inc} = 1;

                    for my $v ( keys %ver_lookup ) {
                        $nss->{ $inc . '::' . $v }{'depends'}{'optmods'}{$item} = 1;
                    }
                }
            }
        }
    }

    my $HoA = {};

    require Cpanel::CPAN::Algorithm::Dependency::Ordered;
    require Cpanel::CPAN::Algorithm::Dependency::Source::HoA;

    for my $item ( keys %{$nss} ) {
        $HoA->{$item} = [];
        if ( ref $nss->{$item}{'depends'}{'optmods'} eq 'HASH' ) {
            for my $pkg ( %{ $nss->{$item}{'depends'}{'optmods'} } ) {
                if ( $nss->{$item}{'depends'}{'optmods'}{$pkg} ) {
                    push @{ $HoA->{$item} }, $pkg;
                }
            }
        }
    }

    if ( exists $self->{'_'}{'additional_depends'} ) {
        for my $ns ( keys %{ $self->{'_'}{'additional_depends'} } ) {
            push @{ $HoA->{$ns} }, @{ $self->{'_'}{'additional_depends'}{$ns} };
        }
    }

    my $dep = Cpanel::CPAN::Algorithm::Dependency::Ordered->new( 'source' => Cpanel::CPAN::Algorithm::Dependency::Source::HoA->new($HoA), );

    $state_hr->{'order'} = $dep->schedule_all();
}

sub get_state_ns_to_easyconfig_hashref {
    my ( $self, $state_hr ) = @_;

    my $main_name = $self->{'state_config'}{'main_name'};

    my $nss = {};
    for my $opt ( keys %{ $state_hr->{'optmods'}{$main_name} } ) {
        if ( $opt =~ m{\.tar\.gz} ) {
            delete $state_hr->{'optmods'}{$main_name}{$opt};
            next;
        }
        if ( $opt eq '_main' ) {
            for my $vers ( keys %{ $state_hr->{'optmods'}{$main_name}{$opt} } ) {
                $nss->{ 'Cpanel::Easy::' . $main_name . '::' . $vers } = $state_hr->{'optmods'}{$main_name}{$opt}{$vers};
            }
        }
        else {
            $nss->{ 'Cpanel::Easy::' . $main_name . '::' . $opt } = $state_hr->{'optmods'}{$main_name}{$opt};
        }
    }

    for my $oth ( keys %{ $state_hr->{'optmods'} } ) {
        if ( $oth =~ m{\.tar\.gz} ) {
            delete $state_hr->{'optmods'}{$oth};
            next;
        }
        next if $oth eq '_main';
        next if $oth eq $main_name;
        $nss->{$oth} = $state_hr->{'optmods'}{$oth};
    }

    # $nss key => value is Full::NS => easyconfig_with_variations

    return $nss;
}

# Determines if the EA state file is "old".  This generally
# means it ought to be refreshed.
sub saved_state_is_old {
    my ($self) = @_;

    # Consider state old if any of the following are TRUE:
    return 1

      #   1. missing state_file, or
      if !-e $self->{'state_file'}

      #   2. zero-byte (e.g. invalid) state file, or
      || -z $self->{'state_file'}

      #   3. file modification time has expired, or
      || time > ( ( stat $self->{'state_file'} )[9] + $self->{'state_file_expire_seconds'} )

      #   4. file not modified, but creation time expired, or
      || ( $self->{'state'}{'created'}
        && time > ( $self->{'state'}{'created'} + $self->{'state_file_expire_seconds'} ) )

      #   5. forced to consider it old, despite the modification time
      || -e $self->{'state_file_needs_redone_file'}

      #   6. option mod custom dir has been updated (default:/var/cpanel/easy/apache/custom_opt_mods), or
      || ( $self->{'state'}{'created'}
        && ( stat $self->{'opt_mod_custom_dir'} )[9] > $self->{'state'}{'created'} )

      #   7. cPanel/WHM version has changed
      || ( -e '/usr/local/cpanel/version'
        && ( ( stat(_) )[9] > ( stat $self->{'state_file'} )[9] ) );

    return;
}

sub include_dir_into_hashref {
    my ( $self, $namespace, $dir, $hashref, $skip, $is_inc ) = @_;

    if ( !$self->is_namespace($namespace) ) {
        $self->log_warn( [q{Argument is not a valid namespace}] );
        return;
    }

    return if !-d $dir;

    if ( opendir my $obj_dh, $dir ) {
        for my $file ( readdir $obj_dh ) {
            next if exists $skip->{$file};
            next if $file !~ m{\.pm$};

            my $ns = $file;
            $ns =~ s{\.pm$}{};

            next if !$is_inc && grep /^$ns$/, @{ $self->{'state_config'}{'main_vers'} };
            my $curn = $namespace . '::' . $ns;

            my $temp = {};
            $self->include_ns_into_hashref( $curn, $temp );

            my $main = 'Cpanel::Easy::' . $self->{'state_config'}{'main_name'} . '::';
            if ( !$is_inc ) {
                $hashref->{'optmods'}{ $self->{'state_config'}{'main_name'} }{$ns} = $temp->{'optmods'}{$curn};
            }
            else {
                $hashref->{'optmods'}{$curn} = $temp->{'optmods'}{$curn};
            }

            if ( $namespace eq 'Cpanel::Easy' ) {
                push @{ $hashref->{'custom_opt_mods_for_main_include_list'} }, $curn;
            }
        }
        closedir $obj_dh;

        if ( $namespace eq 'Cpanel::Easy' && ref $hashref->{'custom_opt_mods_for_main_include_list'} eq 'ARRAY' ) {
            $self->{'state_config'}{'custom_opt_mods_loopkup'} = {} if !exists $self->{'state_config'}{'custom_opt_mods_loopkup'} || ref $self->{'state_config'}{'custom_opt_mods_loopkup'} ne 'HASH';
            @{ $self->{'state_config'}{'custom_opt_mods_loopkup'} }{ @{ $hashref->{'custom_opt_mods_for_main_include_list'} } } = ();    # create lookup hash
            push @{ $self->{'state_config'}{'include'} }, @{ $hashref->{'custom_opt_mods_for_main_include_list'} };
        }
    }
    else {
        $self->log_warn( [ q{Could not opendir() '[_1]': [_2]}, $dir, $! ] );
    }
}

sub include_ns_into_hashref {
    my ( $self, $ns, $hashref, $is_include ) = @_;

    my $hr = $self->get_easyconfig_hr_from_ns_variations( $ns, @{ $self->get_variations( $ns, $is_include ) }, );

    # no need to cache these in YAML:
    delete $hr->{'step'};
    delete $hr->{'dryrun'};

    $hashref->{'optmods'}{$ns} = $hr;
}

sub get_php_optmods_depends {
    my ( $self, $profile_hr ) = @_;

    my $php_optmods_depends = {};

    my $ns = 'Cpanel::Easy::PHP5';
    eval qq{ require $ns; };
    for my $subversion ( $ns->versions() ) {
        if ( $profile_hr->{ $ns . '::' . $subversion } ) {
            $php_optmods_depends->{ $ns . '::' . $subversion } = 1;
        }
    }

    return $php_optmods_depends;
}

1;
