package Config::Model::Dpkg::Dependency ;

use 5.10.1;

use Config::Model 2.066; # for show_message

use Mouse;
use URI::Escape;

use feature qw/postderef signatures/;
no warnings qw/experimental::postderef experimental::signatures/;

# Debian only module
use lib '/usr/share/lintian/lib' ;
use Lintian::Relation ;

use DB_File ;
use Log::Log4perl qw(get_logger :levels);
use Module::CoreList;
use JSON;
use version ;

use Parse::RecDescent ;

# available only in debian. Black magic snatched from
# /usr/share/doc/libapt-pkg-perl/examples/apt-version
use AptPkg::Config '$_config';
use AptPkg::System '$_system';
use AptPkg::Version;
use AptPkg::Cache ;
use LWP::Simple ;

my $madison_host = 'api.ftp-master.debian.org';
my $madison_endpoint = "https://$madison_host/madison";

# list of virtual packages
# See https://www.debian.org/doc/packaging-manuals/virtual-package-names-list.txt
# updated from 30 Jul 2014 version
my @virtual_list = qw/
 adventure
 audio-mixer
 awk
 boom-engine
 boom-wad
 c-compiler
 c-shell
 cron-daemon
 debconf-2.0
 dhcp-client
 dict-client
 dict-server
 dictd-dictionary
 doom-engine
 doom-wad
 dotfile-module
 emacsen
 flexmem
 fonts-japanese-gothic
 fonts-japanese-mincho
 foomatic-data
 fortran77-compiler
 ftp-server
 httpd
 httpd-cgi
 httpd-wsgi
 httpd-wsgi3
 ident-server
 imap-client
 imap-server
 inet-superserver
 info-browser
 ispell-dictionary
 java5-runtime
 java6-runtime
 java7-runtime
 java8-runtime
 java9-runtime
 java5-runtime-headless
 java6-runtime-headless
 java7-runtime-headless
 java8-runtime-headless
 java9-runtime-headless
 kernel-headers
 kernel-image
 kernel-source
 lambdamoo-core
 lambdamoo-server
 libc-dev
 linux-kernel-log-daemon
 lzh-archiver
 mail-reader
 mail-transport-agent
 mailx
 man-browser
 mpd-client
 myspell-dictionary
 news-reader
 news-transport-system
 pdf-preview
 pdf-viewer
 pgp
 pop3-server
 postscript-preview
 postscript-viewer
 radius-server
 rsh-client
 rsh-server
 scheme-ieee-11878-1900
 scheme-r4rs
 scheme-r5rs
 scheme-srfi-0
 scheme-srfi-55
 scheme-srfi-7
 stardict
 stardict-dictdata
 stardict-dictionary
 system-log-daemon
 tclsh
 telnet-client
 telnet-server
 time-daemon
 ups-monitor
 virtual-mysql-client
 virtual-mysql-client-core
 virtual-mysql-server
 virtual-mysql-server-core
 virtual-mysql-testsuite
 wish
 wordlist
 www-browser
 x-audio-mixer
 x-display-manager
 x-session-manager
 x-terminal-emulator
 x-window-manager
 xserver
/;

# other less official virtual packages
push @virtual_list,
  qw/
		libgl-dev
		libtiff-dev
		ruby-interpreter
        ssh-client
        ssh-server
	/;

my %virtual_hash = map {( $_ => 1); } @virtual_list;

my %debian_map;
my $version = \%Module::CoreList::version;

foreach my $v (values %$version) {
    foreach my $pm ( keys %$v ) {
        next unless defined $pm;
        my $k = lc($pm);
        $k =~ s/::/-/g;
        $debian_map{"lib$k-perl"} = $pm;
    }
}

my $logger = get_logger("Tree::Element::Value::Dependency") ;

# initialise the global config object with the default values
$_config->init;

# determine the appropriate system type
$_system = $_config->system;

# fetch a versioning system
my $vs = $_system->versioning;

my $apt_cache = AptPkg::Cache->new ;

# end of AptPkg black magic

extends qw/Config::Model::Value/ ;

my $grammar = << 'EOG' ;

{
    my @dep_errors ;
    my $add_error = sub {
        my ($err, $txt) = @_ ;
        push @dep_errors, "$err: '$txt'" ;
        return ; # to ensure production error
    } ;
}

# comment this out when modifying the grammar
<nocheck>

dependency: { @dep_errors = (); } <reject>

dependency: depend(s /\|/) eofile {
    $return = [ 1 , @{$item[1]} ] ;
  }
  |  {
    push( @dep_errors, "Cannot parse: '$text'" ) unless @dep_errors ;
    $return =  [ 0, @dep_errors ];
  }

depend: pkg_dep | variable

# For the allowed stuff after ${foo}, see #702792
variable: /\$\{[\w:\-]+\}[\w\.\-~+]*/

pkg_dep: pkg_name dep_version(?) arch_restriction(?) profile_restriction(s?) {
    my %ret = ( name => $item{pkg_name} );
    $ret{dep}     = $item[2][0] if @{$item[2]};
    $ret{arch}    = $item[3][0] if @{$item[3]};
    $ret{profile} = $item[4] if @{$item[4]};
    $return = \%ret ;
}

# see https://wiki.debian.org/BuildProfileSpec
profile_restriction: '<' profile(s) '>' { $return = $item[2]; }

profile: not(?) profile_name profile_extention(?) {
  $return = join('', @{$item[1]}, $item{profile_name}, @{$item[3]});
}

profile_extention: '.' /[\w\-]+/ '.' /[\w-]+/ {
    $return = join('', @item[1..4]) ;
}

profile_name: 'cross' | 'pkg' | 'stage1'| 'stage2' |
   'nobiarch' | 'nocheck' | 'nodoc' | 'nogolang' | 'nojava' | 'noperl' | 'nopython' | 'noudeb' |
{
    my @a = ('cross', 'pkg', 'stage1', 'stage2',
            'nobiarch', 'nocheck', 'nodoc', 'nogolang', 'nojava', 'noperl', 'nopython', 'noudeb');
    my ($bad) = split / /, $text;
    $add_error->("Unknown build profile name '$bad'","Expected one of @a") ;
}

arch_restriction: '[' osarch(s) ']'
    {
        my $mismatch = 0;
        my $ref = $item[2] ;
        for (my $i = 0; $i < $#$ref -1 ; $i++ ) {
            $mismatch ||= ($ref->[$i][0] xor $ref->[$i+1][0]) ;
        }
        my @a = map { ($_->[0] || '') . ($_->[1] || '') . $_->[2] } @$ref ;
        if ($mismatch) {
            $add_error->("some names are prepended with '!' while others aren't.", "@a") ;
        }
        else {
            $return = \@a ;
        }
    }

dep_version: '(' oper version ')' { $return = [ $item{oper}, $item{version} ] ;}

pkg_name: /[a-z0-9][a-z0-9\+\-\.]+(?=[\s([|]|\Z)/
    | /\S+/ { $add_error->("bad package name", $item[1]) ;}

oper: '<<' | '<=' | '=' | '>=' | '>>'
    | /\S+/ { $add_error->("bad dependency version operator", $item[1]) ;}

version: variable | /[\w\.\-~:+]+(?=\s|\)|\Z)/
    | /\S+/ { $add_error->("bad dependency version", $item[1]) ;}

# valid arch are listed by dpkg-architecture -L
osarch: not(?) os(?) arch
    {
        $return =  [ $item[1][0], $item[2][0], $item[3] ];
    }

not: '!'

os: /(any|uclibc-linux|linux|kfreebsd|knetbsd|kopensolaris|hurd|darwin|freebsd|netbsd|openbsd|solaris|uclinux)
   -/x
   | /\w+/ '-' { $add_error->("bad os in architecture specification", $item[1]) ;}

arch: / (any |alpha|amd64
         |arm(64|eb|el|hf)?
         |avr32 |hppa |i386 |ia64 |lpia |m32r |m68k
         |mips(el|64el|64)?
         |powerpc(el|spe)?
         |ppc64\b |ppc64el |s390 |s390x
         |sh3\b |sh3eb |sh4\b |sh4eb |sparc\b |sparc64 |x32 )
        (?=(\]| ))
      /x
      | /\w+/ { $add_error->("bad arch in architecture specification", $item[1]) ;}


eofile: /^\Z/

EOG

my $parser ;

sub dep_parser {
    $parser ||= Parse::RecDescent->new($grammar) ;
    return $parser ;
}

# this method may recurse bad:
# check_dep -> meta filter -> control maintainer -> create control class
# autoread started -> read all fileds -> read dependency -> check_dep ...

sub check_value {
    my $self = shift ;
    my %args = @_ > 1 ? @_ : (value => $_[0]) ;

    $args{fix} //= 0;
	# when fixing, SUPER::check_value may modify $args{value} before calling back
    my ($ok, $value) = $self->SUPER::check_value(%args) ;
    return $self->check_dependency(%args, value => $value, ok => $ok) ;
}

sub check_dependency {
    my $self = shift;
    my %args = @_ ;

    my ($value, $check, $silent, $notify_change, $ok, $apply_fix)
        = @args{qw/value check silent notify_change ok fix/} ;

    # value is one dependency, something like "perl ( >= 1.508 )"
    # or exim | mail-transport-agent or gnumach-dev [hurd-i386]

    # see http://www.debian.org/doc/debian-policy/ch-relationships.html

    # to get package list in json format ( 'f' option)
    # wget -q -O - 'https://api.ftp-master.debian.org/madison?package=perl-doc&f'
    #  MOJO_USERAGENT_DEBUG=0 mojo get 'https://api.ftp-master.debian.org/madison?package=perl-doc&f'

    my @dep_chain ;
    if (defined $value) {
        $logger->debug("calling check_depend with Parse::RecDescent with '$value' fix is $apply_fix");
        my $ret = dep_parser->dependency ( $value ) ;
        my $ok = shift @$ret ;
        if ($ok) {
            @dep_chain = @$ret ;
        }
        else {
            $self->add_error(@$ret) ;
        }
    }

    my $old = $value ;
    my @msgs;

    foreach my $dep (@dep_chain) {
        next unless ref($dep) ; # no need to check variables
        $self->check_or_fix_pkg_name($apply_fix, $dep, $old, \@msgs) ;
		$self->check_or_fix_essential_package($apply_fix, $dep, \@msgs) ;
		$self->check_or_fix_dep($apply_fix, $dep, $old, \@msgs) ;
    }


	$self->check_depend_chain($apply_fix, \@dep_chain, $old, \@msgs) ;

    # "ideal" dependency is always computed, but it does not always change
    my $new = $self->struct_to_dep(@dep_chain);

    if ( $logger->is_debug ) {
        my $new //= '<undef>';
        no warnings 'uninitialized';
        $logger->debug( "'$old' done" . ( $apply_fix ? " changed to '$new'" : '' ) );
    }

    {
        no warnings 'uninitialized';
        my $msg = join('; ', @msgs);
        $self->_store_fix( $old, $new, $msg ) if $apply_fix and @msgs and $new ne $old;
    }
    return ($ok, $new) ;
}

sub check_debhelper_version {
    my ($self, $apply_fix, $dep_info, $msgs) = @_ ;
    my $dep_name = $dep_info->{name};
    my ($oper, $dep_v) = @{ $dep_info->{dep} || []};

    my $dep_string = $self->struct_to_dep($dep_info) ;
    my $lintian_dep = Lintian::Relation->new( $dep_string ) ;
    $logger->debug("checking '$dep_string' with lintian");

    # try to create compat_obj, but do not try twice (hence the exists test)
    # compat_obj will be undef with 'cme edit dpkg-control'
    if (not exists $self->{_compat_obj} ) {
        # using mode loose because debian-control model can be used alone
        # and compat is outside of debian-control
        my $c = $self->{_compat_obj} = $self->grab(mode => 'loose', step => "!Dpkg compat") ;
        $c->register_dependency($self) if defined $c;
    }

    return unless defined $self->{_compat_obj};

    my $compat_value = $self->{_compat_obj}->fetch;

    my $min_dep = Lintian::Relation->new("debhelper ( >= $compat_value)") ;

    if ($lintian_dep->implies ($min_dep)) {
        $logger->debug($dep_string." -> ".$lintian_dep->unparse." implies ". $min_dep->unparse);
        return;
    }

    $logger->debug("'$dep_string' does not imply debhelper >= $compat_value");

    # $show_rel avoids undef warnings
    my $show_rel = join(' ', map { $_ || ''} ($oper, $dep_v));
    my $msg = "should be (>= $compat_value) not ($show_rel) because compat is $compat_value" ;
    if ($apply_fix) {
        $dep_info->{dep} = [ '>=', $compat_value] ; # notify_change called in check_value
        $logger->info("fixed debhelper dependency from "
            ."$dep_name $show_rel -> ".$min_dep->unparse." (for compat $compat_value)");
        push $msgs->@*, $msg;
    }
    else {
        $self->{nb_of_fixes}++ ;
        $self->add_warning( $msg );
        $logger->info("will warn: $msg (fix++)");
    }
}

sub struct_to_dep {
    my $self = shift ;
    my @input = @_ ;

    my @alternatives ;
    foreach my $d (@input) {
        my $line = '';

        # empty name is skipped
        if (ref $d) {
            my ($name, $dep,$arch, $prof) = @{$d}{qw/name dep arch profile/} ;
            if ( $name) {
                $line .= $name;

                $line .= " (@$dep)" if defined $dep->[1];

                $line .= " [@$arch]" if $arch;

                if ($prof) {
                    foreach my $prof_or (@$prof) {
                        $line .= ' <'.join(' ',@$prof_or).'>';
                    }
                }
            }
        }
        else {
            $line .= $d;
        }
        push @alternatives, $line if $line ;
    }

    my $actual_dep = @alternatives ? join (' | ',@alternatives) : undef ;

    return $actual_dep ;
}

# @input contains the alternates dependencies (without '|') of one dependency values
# a bit like @input = split /|/, $dependency

# will modify @input (array of ref) when applying fix
sub check_depend_chain {
    my ($self, $apply_fix, $input, $old, $msgs) = @_ ;

    my $actual_dep = $self->struct_to_dep (@$input);
    my $ret = 1 ;

    return 1 unless defined $actual_dep; # may have been cleaned during fix
    $logger->debug("called with $actual_dep with apply_fix $apply_fix");

    foreach my $depend (@$input) {
        if (ref ($depend)) {
            # is a dependency (not a variable a la ${perl-Depends})
            my $dep_name = $depend->{name};
            my ($oper, $dep_v) = @{ $depend->{dep} || []};
            $logger->debug("scanning dependency $dep_name"
                .(defined $dep_v ? " $dep_v" : ''));
            if ($dep_name =~ /lib[\w+\-]+-perl/) {
                $ret &&= $self->check_perl_lib_dep ($apply_fix, $actual_dep, $depend, $input, $msgs);
                last;
            }
        }
    }

    if ($logger->is_debug and $apply_fix) {
        my $str = $self->struct_to_dep(@$input) ;
        $str //= '<undef>' ;
        $logger->debug("new dependency is $str");
    }

    return $ret ;
}

sub extract_cpan_version {
    my ($self,$v) = @_;
    return undef unless defined $v ;
    $v =~ /^(?:\d+:)?([\d\w.-]+?)(?:(?:\+\w+)?-[\w+~]+)?$/ ;
    return $1;
}

# called through check_depend_chain
# does modify $input when applying fix
sub check_perl_lib_dep {
    my ($self, $apply_fix, $actual_dep, $depend, $input, $msgs) = @_;
    my $dep_name = $depend->{name};
    my ($oper, $dep_v) = @{ $depend->{dep} || []};

    $logger->debug("called for $dep_name with $actual_dep with apply_fix $apply_fix");

    my ($old_perl_dep) = grep { $_->{name} eq 'perl' } @$input;
    my $old_perl_versioned_dep = $old_perl_dep->{dep}[1] // '';

    # The dependency should be in the form perl (>= 5.10.1) | libtest-simple-perl (>= 0.88)".
    # cf http://pkg-perl.alioth.debian.org/policy.html#debian_control_handling
    # If the Perl version is not available in sid, the order of the dependency should be reversed
    # libcpan-meta-perl | perl (>= 5.13.10)
    # because Debian build daemons use the first available alternative

    # check for dual life module, module name follows debian convention...
    my $cpan_name = $debian_map{$dep_name};
    return 1 unless $cpan_name ;

    my $first_perl =  Module::CoreList->first_release($cpan_name) ;
    return 1 unless $first_perl;

    my $deprecated = Module::CoreList->deprecated_in($cpan_name) ;
    $logger->debug("dual life $dep_name is deprecated with perl $deprecated") if $deprecated;
    my $removed    = Module::CoreList->removed_from($cpan_name) ;
    $logger->debug("dual life $dep_name is removed from perl $removed") if $removed;

    return 1 if (defined $dep_v && $dep_v =~ m/^\$/) ; # version like ${foobar} in lib dep

    my %ideal_perl_dep ;
    my %ideal_lib_dep ;

    my @res = $self->get_available_version( $dep_name);

    # check version for the first available version in Debian: debian
    # dep may have no version specified but older versions can be found
    # in CPAN that were never packaged in Debian

	# get_available_version returns oldest first, like (etch,1.2,...)
	my ($oldest_debian_with_lib,$oldest_lib_version_in_debian) = @res[0,1] ;
	if (not defined $oldest_lib_version_in_debian or not defined $oldest_debian_with_lib) {
		# no need to check further.
		return 1;
	}

	# lob off debian release number
	$oldest_lib_version_in_debian =~ s/-.*//;
	my $check_v = $dep_v ;

	# use oldest version only if the oldest version is NOT in oldstable
	# second test can be removed end of April 2016 (cache expiry)
    # but cached data for tests must be modified to respect the new convention
	if (   $oldest_debian_with_lib !~ /oldstable/
        or $oldest_debian_with_lib =~ /wheezy|jessie|stretch|buster|sid/
    ) {
		$check_v ||= $oldest_lib_version_in_debian ;
		$logger->debug("dual life $dep_name has oldest debian $oldest_lib_version_in_debian, using $check_v");
	}

	my $cpan_dep_v = $self->extract_cpan_version($check_v);

	my $v_decimal = Module::CoreList->first_release(
		$cpan_name,
		version->parse( $cpan_dep_v )
	);

    # return when module is not and was never part of core
	return 1 unless defined $v_decimal;

	my $v_normal = version->new($v_decimal)->normal;
	$v_normal =~ s/^v//;    # loose the v prefix
	if ( $logger->is_debug ) {
		my $dep_str = $dep_name . ( defined $check_v ? ' ' . $check_v : '' );
		$logger->debug("dual life $dep_str found in Perl core $v_normal (req perl is $old_perl_versioned_dep)");
	}

    if ( not defined $dep_v and $old_perl_versioned_dep and $old_perl_versioned_dep gt $v_decimal) {
        # when alternate lib version is not defined (because the requirement is satisfied by all
        # available versions of the lib), the actual requirement is held by the versioned dep of perl
        # hence, it must be preserved
        my $p_data = Module::CoreList->find_version($v_decimal);
        $dep_v = $p_data->{$cpan_name};
        $depend->{dep} = [ '>=', $dep_v ];
        $logger->debug("preserving old perl versioned dep $old_perl_versioned_dep instead of $v_normal"
                           ." for $dep_name (implies dep > $dep_v)");
        $v_normal = $old_perl_versioned_dep;
    }


    my ($has_older_perl) = $self->check_versioned_dep( { name => 'perl', dep => ['>=', $v_normal]} );
    my ($has_older_lib)  = $self->check_versioned_dep( $depend );

    # Perl module is being or was removed from core
    if ($deprecated or $removed) {
        if ( $has_older_perl ) {
            my $max_ver_normal = version->new($deprecated)->normal ;
            $max_ver_normal =~ s/^v//;
            $logger->debug("Found older perl for a soon to be removed lib. Using perl << $max_ver_normal dependency'");
            $ideal_perl_dep{name} = 'perl';
            $ideal_perl_dep{dep} = [ '<<', $max_ver_normal ];
        }
        else {
            # e.g. perl (<< 5.8.0) i.e. lower than obsolete perl -> remove dependency from ideal perl dep
            $logger->debug("Did not find older perl for an obsolete module. Perl dep should be removed");
        }

        # always specify the library dependency
        $ideal_lib_dep{name} = $dep_name;
        $ideal_lib_dep{dep} = [ '>=', $dep_v ] if $dep_v and $has_older_lib;

    }
    else {
        # Perl module is part of core (this was tested before)
        $ideal_perl_dep{name} = 'perl';
        if ( $has_older_perl ) {
            $logger->debug("Found older perl for a greater-than perl dep. Ideal perl dep is 'perl >= $v_normal'");
            $ideal_perl_dep{dep} = [ '>=', $v_normal ];
        }
        else {
            $logger->debug("Did no find older perl for a greater-than perl dep. Ideal perl dep is plain 'perl'");
        }

        # we need to specify the libray dependency even this one has no
        # version requirement and the dependency is satisfied by Perl core
        # because dual dependency will eventually be removed. Better keep this info
        $ideal_lib_dep{name} = $dep_name;

        if ($dep_v) {
            $ideal_lib_dep{dep} = [ '>=', $dep_v ] if $dep_v and $has_older_lib;
        }
    }

    my %perl_version =  $self->get_available_version( 'perl');
    my $sid_perl_version = $perl_version{unstable} || $perl_version{sid} ;
    my $has_older_perl_in_sid = ( $vs->compare( $v_normal, $sid_perl_version) < 0 ) ? 1 : 0;
    $logger->debug(
		"perl $v_normal is",
		$has_older_perl_in_sid ? ' ' : ' not ',
		"older than perl in sid ($sid_perl_version)"
	);

    my $perl_dep_first = $has_older_perl_in_sid && ! $deprecated;

	my @pre_ordered_ideal_dep
        = $perl_dep_first   ? ( \%ideal_perl_dep, \%ideal_lib_dep  )
        :                     ( \%ideal_lib_dep,  \%ideal_perl_dep ) ;

	my @ordered_ideal_dep = grep {defined $_->{name} } @pre_ordered_ideal_dep;

	my $ideal_dep = $self->struct_to_dep( @ordered_ideal_dep );

    die "Internal error: undefined ideal dep. Please report bug with the dependencies that triggered the bug"
        unless defined $ideal_dep;

	if ( $actual_dep ne $ideal_dep ) {
        my $msg = "Dependency of dual life package should be '$ideal_dep' not '$actual_dep'";
		if ($apply_fix) {
			@$input =  @ordered_ideal_dep ; # notify_change called in check_value
            if ($logger->is_info) {
                $logger->info("fixed dependency with: '$ideal_dep', was '$actual_dep'");
            }
            push $msgs->@*, $msg;
		}
		else {
			$self->{nb_of_fixes}++;
			my $msg = "Dependency of dual life package should be '$ideal_dep' not '$actual_dep'";
            if ($deprecated) {
                $msg .= " (deprecated from perl $deprecated)" ;
            }
            if ($removed) {
                $msg .= " (removed from perl $removed)" ;
            }
			$self->add_warning ($msg);
			$logger->info("will warn: $msg (fix++)");
		}
		return 0;
	}

    return 1 ;
}

sub check_versioned_dep {
    my ($self ,$dep_info) = @_ ;
    my $pkg = $dep_info->{name};
    my ($oper, $vers) = @{ $dep_info->{dep} || []};
    $logger->debug("called with '" . $self->struct_to_dep($dep_info) ."'") if $logger->is_debug;

    # special case to keep lintian happy
    return (1) if $pkg eq 'debhelper' ;

    # check if Debian has version older than required version
    my @dist_version = $self->get_available_version( $pkg) ;

	if ( @dist_version  # no older for unknow packages
		 and defined $oper
		 and $vers !~ /^\$/  # a dpkg variable
	 ) {
		my $src_pkg_name = $self->grab_value("!Dpkg::Control source Source") ;

		return ($self->has_older_version_than ($pkg, $vers, \@dist_version ));
	}
	else {
		return (1) ;
	}
}

sub has_older_version_than {
    my ($self, $pkg, $vers, $dist_version ) = @_;

    my @list ;
    my $has_older = 0;
    while (@$dist_version) {
        my ($d,$v) = splice @$dist_version,0,2 ;

        push @list, "$d -> $v;" ;

        if ($vs->compare($vers,$v) > 0 ) {
            $has_older = 1 ;
        }
    }

    $logger->debug("$pkg $vers has_older is $has_older (@list)");

    return 1 if $has_older ;
    return (0,@list) ;
}

#
# New subroutine "check_essential_package" extracted - Thu Aug 30 14:14:32 2012.
#
sub check_or_fix_essential_package {
    my ( $self, $apply_fix, $dep_info, $msgs ) = @_;
    my $pkg = $dep_info->{name};
    my ($oper, $vers) = @{ $dep_info->{dep} || []};
    $logger->debug("called with '", scalar $self->struct_to_dep($dep_info), "' and fix $apply_fix") if $logger->is_debug;

    # Remove unversioned dependency on essential package (Debian bug 684208)
    # see /usr/share/doc/libapt-pkg-perl/examples/apt-cache

    my $cache_item = $apt_cache->get($pkg);
    my $is_essential = 0;
    $is_essential++ if (defined $cache_item and $cache_item->get('Flags') =~ /essential/i);

    if ($is_essential and not defined $oper) {
        $logger->debug( "found unversioned dependency on essential package: $pkg");
        my $msg = "unnecessary unversioned dependency on essential package: $pkg";
        if ($apply_fix) {
            %$dep_info = ();
            $logger->info("fix: removed unversioned essential dependency on $pkg");
            push $msgs->@*, $msg;
        }
        else {
            $self->add_warning($msg);
            $self->{nb_of_fixes}++;
            $logger->info("will warn: $msg (fix++)");
        }
    }
}


my %pkg_replace = (
    'perl-module' => 'perl' ,
) ;

sub check_or_fix_pkg_name {
    my ( $self, $apply_fix, $dep_info, $old, $msgs ) = @_;
    my $pkg = $dep_info->{name};

    $logger->debug("called with '", scalar $self->struct_to_dep($dep_info), "' and fix $apply_fix")
        if $logger->is_debug;

    my $new = $pkg_replace{$pkg} ;
    if ( $new ) {
        my $msg = "dubious package name: $pkg. Preferred package is $new";
        if ($apply_fix) {
            $logger->info("fix: changed package name from $pkg to $new");
            $dep_info->[0] = $pkg = $new;
            push $msgs->@*, $msg;
        }
        else {
            $self-> add_warning ($msg);
            $self->{nb_of_fixes}++;
            $logger->info("will warn: $msg (fix++)");
        }
    }

    # check if this package is defined in current control file
    if ($self->grab(step => "- - binary:$pkg", qw/mode loose autoadd 0/)) {
        $logger->debug("dependency $pkg provided in control file") ;
    }
    else {
        my @res = $self->get_available_version(  $pkg );
		if ( @res == 0 and not $virtual_hash{$pkg}) {
			# no version found for $pkg
			# don't know how to distinguish virtual package from source package
			$logger->debug("unknown package $pkg");
			$self->add_warning(
				"package $pkg is unknown. Check for typos if not a virtual package.");
		}
    }
}

sub check_or_fix_dep {
    my ( $self, $apply_fix, $dep_info, $old, $msgs ) = @_;
    my $pkg = $dep_info->{name};

    $logger->debug("called with '", scalar $self->struct_to_dep($dep_info), "' and fix $apply_fix")
        if $logger->is_debug;

    if (not defined $pkg) {
        # pkg may be cleaned up during fix
        return;
    }

    if ( $pkg eq 'debhelper' ) {
        return $self->check_debhelper_version( $apply_fix, $dep_info, $msgs );
    }

    my ( $vers_dep_ok, @list ) =  $self->check_versioned_dep( $dep_info );
    return if $vers_dep_ok;

    if ($dep_info->{dep}[0] =~ /</) {
        $self->warn_or_remove_dep ($apply_fix, $dep_info, \@list, $msgs) ;
    }
    else {
        $self->warn_or_remove_vers_dep ($apply_fix, $dep_info, \@list, $msgs) ;
    }
}

sub warn_or_remove_vers_dep {
    my ( $self, $apply_fix, $dep_info, $list, $msgs ) = @_;
    my $pkg = $dep_info->{name};

    my $msg = "unnecessary greater-than versioned dependency: ". $self-> struct_to_dep($dep_info)
        . ". Debian has @$list";
    if ($apply_fix) {
        delete $dep_info->{dep};    # remove greater-than versioned dep, notify_change called in check_value
        $logger->info("fix: removed greater-than versioned dependency from $pkg");
        push $msgs->@*, $msg;
    }
    else {
        $self->{nb_of_fixes}++;
        $self->add_warning( $msg);
        $logger->info("will warn: $msg (fix++)");
    }
}

sub warn_or_remove_dep {
    my ( $self, $apply_fix, $dep_info, $list, $msgs ) = @_;
    my $pkg = $dep_info->{name};

    my $msg = "unnecessary older-than versioned dependency: ". $self-> struct_to_dep($dep_info)
        . ". Debian has @$list";
    if ($apply_fix) {
        %$dep_info = ();    # remove whole dependency
        $logger->info("fix: removed dependency $pkg");
        push $msgs->@*, $msg;
    }
    else {
        $self->{nb_of_fixes}++;
        $self->add_warning( $msg);
        $logger->info("will warn: $msg (fix++)");
    }
}

use vars qw/%cache $use_test_cache/ ;

# Set up persistence
my $cache_file_name = $ENV{HOME}.'/.config_model_depend_cache' ;

# this condition is used during tests
if (not $use_test_cache) {
    tie %cache => 'DB_File', $cache_file_name,
}

# required to write data back to DB_File
END {
    untie %cache unless $use_test_cache;
}

my $cache_expire_date = time - 24 * 60 * 60 * 7 ;
sub get_available_version {
    my ($self, $pkg_name) = @_ ;

    $logger->debug("called on $pkg_name");

    # don't query info for known virtual package
    if ($virtual_hash{$pkg_name}) {
        $logger->debug("$pkg_name is a known virtual package");
        return ();
    }

    # needed to test unknown package without network
    if (exists $cache{$pkg_name} and not defined $cache{$pkg_name}) {
        $logger->debug("$pkg_name is an unknown package (for test only)");
        return ();
    }

    my ($time,@res) = split / /, ($cache{$pkg_name} || '');
    if (defined $time and $time =~ /^\d+$/ and $time > $cache_expire_date ) {
        $logger->debug("using cached info for $pkg_name");
        return @res;
    }

    my $url = "$madison_endpoint?package=".uri_escape($pkg_name).'&f&b=deb' ;
    $self->instance->show_message("Connecting to $madison_host to check $pkg_name versions. Please wait...") ;
	my $body = get($url);
    my $res ;
	if (defined $body) {
        my $ref = extract_madison_info($body);
        $self->instance->show_message("got info for $pkg_name") ;
        $res = $ref->{$pkg_name} || [];
        $logger->debug("pkg info is @$res");
    }
    else {
        warn "cannot get data for package $pkg_name. Check your proxy ?\n" unless defined $body ;
    }

	return $res->@*;
}


# this function queries *once* madison for package info not found in cache.
# it should be called once when parsing control file
sub cache_info_from_madison {
    my ($instance,@pkg_names) = @_ ;

    $logger->debug("called on @pkg_names");

    my $necessary = 0;
    my @needed;

    foreach my $pkg_name (@pkg_names) {
        next if $virtual_hash{$pkg_name} ; # skip known virtual package
        my ($time,@res) = split / /, ($cache{$pkg_name} || '');
        if (defined $time and $time =~ /^\d+$/ and $time > $cache_expire_date) {
            $logger->debug("using cached info for $pkg_name");
        }
        else {
            push @needed, $pkg_name;
            $necessary++;
        }
    }

    if (not $necessary) {
        return;
    }

    my $url = "$madison_endpoint?package=".uri_escape(join(' ',@needed)).'&f&b=deb' ;
    $instance->show_message(
        "Connecting to $madison_host to check ", scalar @needed, " package versions. Please wait..."
    );
	my $body = get($url);

	if (defined $body) {
        my $res = extract_madison_info($body);
        $instance->show_message( "Got info from $madison_host for ", scalar keys %$res, " packages.") ;
    }
    else {
        warn "cannot get data from madison. Check your proxy ?\n";
    }
}

# See https://ftp-master.debian.org/epydoc/dakweb.queries.madison-module.html
sub extract_madison_info ($json) {
	my %ref ;
    my $json_data = decode_json($json);
    my $data = $json_data->[0] ;

	foreach my $name ( keys $data->%* ) {
        my %avail;
        foreach my $dist (keys $data->{$name}->%*) {
            foreach my $available_v (keys $data->{$name}{$dist}->%*) {
                my $arches = $data->{$name}{$dist}{$available_v}{architectures};
                # see #841667: relevant pkg version is found in arch all or arch amd64
                my @keep = grep { $_ eq 'all' or $_ eq 'amd64'} $arches->@*;

                # the same version may be available in several
                # distributions (testing and unstable are more likely
                # to have the same version for a package)
                $avail{$available_v} //= [];
                push $avail{$available_v}->@*, $dist, $available_v if @keep;
            }
        }

        # @res contains something like 'oldstable 5.10.1-17 stable 5.14.2-21 testing 5.18.1-3 unstable 5.18.1-4'
        my @res = map { $avail{$_}->@* ; } sort { $vs->compare($a,$b) } keys %avail ;

        $ref{$name} = \@res ;
        $cache{$name} = join(' ',time, @res) ;
	}

    return \%ref;
}

__PACKAGE__->meta->make_immutable;

1;

=head1 NAME

Config::Model::Dpkg::Dependency - Checks Debian dependency declarations

=head1 SYNOPSIS

 use Config::Model ;
 use Log::Log4perl qw(:easy) ;
 use Data::Dumper ;

 Log::Log4perl->easy_init($WARN);

 # define configuration tree object
 my $model = Config::Model->new ;
 $model ->create_config_class (
    name => "MyClass",
    element => [
        Depends => {
            'type'       => 'leaf',
            'value_type' => 'uniline',
            class => 'Config::Model::Dpkg::Dependency',
        },
    ],
 ) ;

 my $inst = $model->instance(root_class_name => 'MyClass' );

 my $root = $inst->config_root ;

 $root->load( 'Depends="libc6 ( >= 1.0 )"') ;
 # Connecting to qa.debian.org to check libc6 versions. Please wait ...
 # Warning in 'Depends' value 'libc6 ( >= 1.0 )': unnecessary
 # versioned dependency: >= 1.0. Debian has lenny-security ->
 # 2.7-18lenny6; lenny -> 2.7-18lenny7; squeeze-security ->
 # 2.11.2-6+squeeze1; squeeze -> 2.11.2-10; wheezy -> 2.11.2-10; sid
 # -> 2.11.2-10; sid -> 2.11.2-11;

=head1 DESCRIPTION

This class is derived from L<Config::Model::Value>. Its purpose is to
check the value of a Debian package dependency for the following:

=over

=item *

syntax as described in http://www.debian.org/doc/debian-policy/ch-relationships.html

=item *

Whether the version specified with C<< > >> or C<< >= >> is necessary.
This module checks with Debian server whether older versions can be
found in Debian old-stable or not. If no older version can be found, a
warning is issued (unless the package is known to be virtual)

=item *

Whether a Perl library is dual life. In this case the dependency is checked according to
L<Debian Perl policy|http://pkg-perl.alioth.debian.org/policy.html#debian_control_handling>.
Because Debian auto-build systems (buildd) will use the first available alternative,
the dependency should be in the form :

=over

=item *

C<< perl (>= 5.10.1) | libtest-simple-perl (>= 0.88) >> when
the required perl version is available in sid. ".

=item *

C<< libcpan-meta-perl | perl (>= 5.13.10) >> when the Perl version is not available in sid

=back

=back

=head1 Cache

Queries to Debian server are cached in C<~/.config_model_depend_cache>
for about one month.

=head1 BUGS

=over

=item *

Virtual package names are found scanning local apt cache. Hence an unknown package
on your system may a virtual package on another system.

=item *

More advanced checks can probably be implemented. The author is open to
new ideas. He's even more open to patches (with tests).

=back

=head1 AUTHOR

Dominique Dumont, ddumont [AT] cpan [DOT] org

=head1 SEE ALSO

L<Config::Model>,
L<Config::Model::Value>,
L<Memoize>,
L<Memoize::Expire>
