# Copyright 1999-2012. Parallels IP Holdings GmbH. All Rights Reserved.
package Dumper;

use strict;
use warnings;

use AgentConfig;
use Logging;
use Parser;
use SimpleDbConnect;
use File::Spec;
use EnsimGuidGenerator;
use DumperUtils;
use DomainDumper;
use MySQLServer;
use PanelConfigFile;
use PanelDb;

use constant USERNAME_MANGLE_MAX_ITERATIONS => 10000;

my $_db;
my $_clientDb;
my $_mysqlRootPassword;

sub _getClientDb() {
  unless (defined($_clientDb)) {
    $_clientDb = SimpleDbConnect->new('mysql', getDbAdminCredentials(), 'mysql', undef);
  }

  return $_clientDb;
}

# @return list (<username> <password>)
sub getDbAdminCredentials {
  $_mysqlRootPassword ||= _getMysqlPassword(); 
  return ('root', $_mysqlRootPassword);
}

# This function uses internal PPCPL library for reading MySQL root password
sub _getMysqlPassword {
  my $python = AgentConfig::pythonBin();
  my $script =<< "EOS"
import sys
sys.path.append('/usr/lib/opcenter/mysql')
from mysqlbe import read_mysqlpass

sys.stdout.write(read_mysqlpass())
EOS
;
	unless (open(PY, "$python -c \"$script\" |")) {
		return '';
	}
	binmode PY;

	my $passwd = <PY>;
	close(PY);
	chomp $passwd;

	return $passwd;
}

sub getResellers {
  Logging::trace('Getting resellers list...');

  # TODO ddidenkov@ revise WHERE clause
  #  Compare with old clause (taken from EnsimX.pl):
  # " WHERE (reseller_info.reseller_id <> 0) OR (site_id IS NOT NULL)";
  my $sql =
      "SELECT DISTINCT reseller_info.username"
    . " FROM reseller_info LEFT JOIN reseller"
    . " ON reseller_info.reseller_id = reseller.reseller_id"
    . " WHERE (reseller_info.reseller_id <> 0)";
  my $wrapDbh = PanelDb::_getDb()->getDbh();
  my @resellers;
  if ( $wrapDbh->{'EXECUTE'}->($sql) ) {
    my $ptrRow;
    while ( $ptrRow = $wrapDbh->{'FETCHROW'}->() ) {
      my @resellerInfo = @$ptrRow;
      push @resellers, $resellerInfo[0];
    }
  }

  $wrapDbh->{'FINISH'}->();
  Logging::debug( "Found " . scalar(@resellers) . " resellers: @resellers" );
  return @resellers;
}

sub getClientIdsForReseller {

  # Could be 'undef' for admin
  my ($reseller) = @_;

  Logging::trace(
    "Getting client identifiers for '" . ( defined($reseller) ? $reseller : 'undef' ) . "' reseller..." );

 # temporary "resellerId by username" resolving to avoid current DumpComposer's cPanel-ish behaviour
  my $resellerId;
  my $wrapDbh = PanelDb::_getDb()->getDbh();
  if ( defined $reseller ) {
    my $sql = "SELECT reseller_id FROM reseller_info WHERE username = '$reseller'";
    if ( $wrapDbh->{'EXECUTE'}->($sql) ) {
      $resellerId = $wrapDbh->{'FETCHROW'}->()->[0];
      $wrapDbh->{'FINISH'}->();
    }
    else {
      Logging::warning("Unable to get reseller id by its username '$reseller'!");

      # Couldn't find any sites for unknown reseller -> return
      return ();
    }
  }
  else {

    # PPCP uses resellerId = 0 to identify admin sites
    $resellerId = 0;
  }

  my $sql =
      "SELECT domain FROM siteinfo"
    . " NATURAL JOIN reseller"
    . " WHERE (reseller.reseller_id = '$resellerId')";
  my @siteAdmins;
  if ( $wrapDbh->{'EXECUTE'}->($sql) ) {
    my $ptrRow;
    while ( $ptrRow = $wrapDbh->{'FETCHROW'}->() ) {
      my @siteInfo = @$ptrRow;
      push @siteAdmins, $siteInfo[0];
    }
  }

  $wrapDbh->{'FINISH'}->();

  Logging::debug( "Found " . scalar(@siteAdmins) . " site admins for '$resellerId' reseller: @siteAdmins" );

  return @siteAdmins;
}

sub getResellerGeneralInfo {
  my $reseller = shift;

  Logging::trace("Getting general info about '$reseller' reseller...");

  my $sql =
      "SELECT reseller_id, username, email, password, fullname, enabled"
    . " FROM reseller_info WHERE (username = '$reseller')";

  my $wrapDbh = PanelDb::_getDb()->getDbh();
  my $ptrHash;
  if ( $wrapDbh->{'EXECUTE'}->($sql) ) {
    $ptrHash = $wrapDbh->{'FETCHHASH'}->();
  }
  $wrapDbh->{'FINISH'}->();
  if ( defined($ptrHash) && ref($ptrHash) =~ /HASH/ ) {
    return $ptrHash;
  }
  else {
    return;
  }
}

sub _getResellerIdByName {
  my ( $reseller ) = @_;
  return PanelDb::_getDb()->queryOneValue(
    "SELECT reseller_id FROM reseller_info WHERE username = '$reseller'",
    'reseller_id'
  );
}

sub getResellerLimits {
  my ( $reseller ) = @_;

  my $limits = {
    # units field of reseller_diskquota is ignored because 
    # it is always set to 'MB' however total_quota is in bytes
    # the same applies for other similar values
    'diskquota' => { 
      'table' => 'reseller_diskquota', 
      'enabled_column' => 'enabled_diskquota', 
      'value_column' => 'total_quota'
    },
    'users' => {
      'table' => 'reseller_users',
      'enabled_column' => 'enabled_users',
      'value_column' => 'total_maxusers'
    },
    'bandwidth' => {
      'table' => 'reseller_bandwidth',
      'enabled_column' => 'enabled_bandwidth',
      'value_column' => 'total_threshold'
    },
    'ip_based_sites' => {
      'table' => 'reseller_ipinfo',
      'enabled_column' => 'enabled_ipinfo_ip',
      'value_column' => 'total_ipbased'
    },
    'name_based_sites' => {
      'table' => 'reseller_ipinfo',
      'enabled_column' => 'enabled_ipinfo_nb',
      'value_column' => 'total_namebased' 
    }
  };
  
  return _getObjectLimits($limits, 'reseller_info', 'reseller_id', _getResellerIdByName($reseller));
}

sub getSiteLimits {
  my ( $domainName ) = @_;

  my $limits = {
    'diskquota' => { 
      'table' => 'diskquota', 
      'enabled_column' => 'enabled', 
      'value_column' => 'quota'
    },
    'users' => {
      'table' => 'users',
      'enabled_column' => 'enabled',
      'value_column' => 'maxusers'
    }
  };

  my $siteId = DomainDumper::getSiteId($domainName);
  my %limits = _getObjectLimits($limits, 'siteinfo', 'site_id', $siteId);

  # bandwidth has no special limit enabled column,
  # so we have to get it in quite another way
  $limits{'bandwidth'} = DomainDumper::getBandwidthLimit($domainName);

  return %limits;
}

sub _getObjectLimits {
  my ( $limits, $mainTable, $tableKeyName, $tableKeyValue ) = @_;

  my @columnExprs;
  my @joinExprs;

  foreach my $limitName ( keys %{$limits}) {
    my $limit = $limits->{$limitName};

    my $table = $limit->{'table'};
    my $enabledColumn = $limit->{'enabled_column'};
    my $valueColumn = $limit->{'value_column'};

    my $sqlTableName = "t_$limitName";

    push(@columnExprs, "$sqlTableName.$enabledColumn as ${limitName}_enabled");
    push(@columnExprs, "$sqlTableName.$valueColumn as ${limitName}_value");

    push(@joinExprs, "INNER JOIN $table $sqlTableName ON $sqlTableName.$tableKeyName = $mainTable.$tableKeyName");
  }

  my $query = 
    'SELECT ' . join(', ', @columnExprs) . 
    " FROM $mainTable " . join(' ', @joinExprs) . 
    " WHERE $mainTable.$tableKeyName = '$tableKeyValue'";

  my $queryResult = PanelDb::_getDb()->queryOneRow($query);
 
  my %result;

  foreach my $limitName ( keys %{$limits} ) {
    if ( $queryResult->{$limitName . '_enabled'} == '1' ) {
      $result{$limitName} = $queryResult->{$limitName . '_value'};
    } else {
      $result{$limitName} = undef;
    }
  }

  return %result;
}

sub canResellerManageDNS {
  my ( $reseller ) = @_;

  return PanelDb::_getDb()->queryOneValue(
    "SELECT enabled_zone_mgmt FROM reseller_bind" .
    " INNER JOIN reseller_info ON reseller_bind.reseller_id = reseller_info.reseller_id" .
    " WHERE reseller_info.username = '$reseller'"
  );
}

sub getAgentStatus {
  my $root = XmlNode->new('agent-status');

  unless ( checkPanelVersion() && defined AgentConfig::iconvBin() ) {
    my $item;
    if ( defined AgentConfig::iconvBin() ) {
      $item = XmlNode->new('wrong-platform');
      $item->setText('');
    }
    else {
      $item = XmlNode->new('wrong-platform');
      $item->setText('no iconv found on the source host');
    }

    $root->addChild($item);
  }

  return $root;
}

sub getPPCPVersion {

# TODO ddidenkov@ revise this check after PPCP source code analysis (the same thing is for scoup.pl)
# TODO ddidenkov@ discuss the found ways with current PPCP developers
# I see several ways in the PPCP's source code:
# 1. cat /usr/lib/opcenter/VERSION (returns '10.3.4-rhel.5ES.9' in my case). Ref: epl/eplapi/implementors/server/server.py, L35, getEPLVersionInfo
# 2. /bin/rpm -qf --qf %{VERSION}-%{RELEASE} /etc/rc.d/init.d/epld (returns the same '10.3.4-rhel.5ES.9'). Ref: epl/eplapi/licensing/licenselib.py, L115, get_version
# 3. /usr/bin/getapplversion.old . It reads the same /usr/lib/opcenter/VERSION file
# 4. python -c "import sys; sys.path.append('/usr/lib/ensim'); import eplapi.implementors.server.server; print eplapi.implementors.server.server.epl_version_release".
#    This way is used by web-interface to show CP version on the dashboard/Server Administrator page.
#    It reads some string generated during the release build.
  my $main_version_file_name = '/usr/lib/opcenter/VERSION';
  if ( -e $main_version_file_name ) {
    my $version = `cat $main_version_file_name`;
    chomp $version;
    if ( $version =~ /(\d+\.\d+(\.\d+)?)-.*/ ) {
      Logging::debug("Parsed '$1' product version number from '$version' string.");
      return $1;
    }
    else {
      Logging::debug("Couldn't get the product version number from '$version'");
    }
  }
  else {
    Logging::debug("The version file '$main_version_file_name' doesn't exist.");
  }

  {
    my $version =
`python -c "import sys; sys.path.append('/usr/lib/ensim'); import eplapi.implementors.server.server; print eplapi.implementors.server.server.epl_version_release"`;
    chomp $version;
    if ( $version =~ /(\d+\.\d+(\.\d+)?)-.*/ ) {
      Logging::debug("Parsed '$1' product version number from '$version' string.");
      return $1;
    }
    else {
      Logging::debug("Couldn't get the product version number from '$version'");
    }
  }

  my $rpm     = AgentConfig::rpmBin();
  my $version = `$rpm -qf --qf \%{VERSION} /etc/rc.d/init.d/epld 2>/dev/null`;
  if ( $version =~ /^(\d+\.\d+(\.\d+)?)$/ ) {
    Logging::debug("Parsed '$1' product version number from '$version' string.");
    return $1;
  }
  else {
    Logging::debug("Couldn't get the product version number from '$version'");
  }

  # there are older ways to determine the version below
  my @rpms_list = ( 'webppliance-version', 'webppliance-ipinfo' );
  for my $rpm_name (@rpms_list) {
    my $version = `$rpm -q $rpm_name 2>/dev/null`;
    if ( $version =~ /^$rpm_name-([^-]+)-[^-]+$/ ) {
      Logging::debug("Parsed '$1' product version number from '$version' string.");
      return $1;
    }
    else {
      Logging::debug("Couldn't get the product version number from '$version'");
    }
  }

  my @version_files_list = ( '/usr/bin/getapplversion', '/usr/bin/getapplversion.old' );
  for my $version_file_name (@version_files_list) {
    if ( -e $version_file_name ) {
      my $version = `$version_file_name`;
      chomp $version;
      if ( $version =~ /(\d+\.\d+(\.\d+)?)-.*/ ) {
        Logging::debug("Parsed '$1' product version number from '$version' string.");
        return $1;
      }
      else {
        Logging::debug("Couldn't get the product version number from '$version'");
      }
    }
    else {
      Logging::debug("The version file '$version_file_name' doesn't exist.");
    }
  }
  Logging::debug("Couldn't detect a PPCP.");
  return;
}

sub checkPanelVersion {
  my $version = getPPCPVersion();

  if ( $version =~ /^(\d+)\.(\d+)/ ) {
    Logging::debug(
      "The product version number '$version'" . " contains major($1) and minor($2) parts." );

    # Check version major and minor fields
    if ( $1 eq '10' && $2 eq '3' ) {
      return $version;
    }
    else {
      Logging::info("The PPCP version '$version' is not supported.");
      return;
    }
  }
  Logging::info("Couldn't determine major and minor version numbers from '$version'.");
  return;
}

sub getProcmailrcAction {
    my ($procmailrcFile) = @_;

    my $action = "mark";  # default value

    open(INPUTPROCCFG, $procmailrcFile) || return $action;
    while (<INPUTPROCCFG>) {
        chomp;
        if (/^SPAM_FOLDER="(.+)"$/) {
            if ($1 eq "\$HOME/mail/Spam") {
                $action = "move";
            } elsif ($1 eq "/dev/null") {
                $action = "delete";
            }
            last;
        }
    }
    close INPUTPROCCFG;

    return $action;
}

sub getSpamassassinMaxThreads {
  # we don't claim this setting is migrated and therefore don't fight to retrieve it.
  # if unable to open both known spamassassin settings files (in this order) - count it unspecified.
  open(SETTINGS_FILE, '/etc/sysconfig/spamassassin') ||
    open(SETTINGS_FILE, '/etc/init.d/spamassassin') ||
      return undef;

  while (<SETTINGS_FILE>) {
    if (/^[[:blank:]]*SPAMDOPTIONS=".*-m[[:blank:]]*(\d*)/) {
      return $1;
    }
  }
  close(SETTINGS_FILE);

  return undef;
}

sub mangleDbUserName {
  my ($userName) = @_;

  my $mangledUserName = $userName;
  if ($userName eq 'admin') {
    my $db = _getClientDb();
    my $suffix = 0;
    for($suffix = 0; $suffix < USERNAME_MANGLE_MAX_ITERATIONS; $suffix += 1) {
      # we should check 16-symbol limit here, but it's met automatically
      $mangledUserName = $userName . '_' . $suffix;
      my $dbCount = MySQLServer::getUserDatabaseNames($mangledUserName);
      if (($dbCount) < 1) {
        last;
      }
    }
    unless ($suffix < USERNAME_MANGLE_MAX_ITERATIONS) {
      Logging::error("Unable to mangle DB user name after '" . USERNAME_MANGLE_MAX_ITERATIONS . "' tries. Please contact support for assistance.");
      # leave last tried username here, because it's still better than 'admin' collision
    }
  }
  return $mangledUserName;
}

sub getDuplicatedLogins {
  my $sql = 'SELECT admin_user, COUNT(*) AS duplicates FROM siteinfo GROUP BY admin_user HAVING COUNT(*) > 1';
  return PanelDb::_getDb()->queryList($sql);
}

my $_namedStatus = undef;

sub getLocalNamedStatus {
  return $_namedStatus if defined ($_namedStatus);

  my $namedStatusCommand = '/sbin/service named status';

  if (-f '/sbin/service') {
    chomp(my $namedStatusWord = `/sbin/service named status`);
    Logging::trace("'$namedStatusCommand' returned '$namedStatusWord'");
    if ($namedStatusWord eq 'running') {
      $_namedStatus = 1;
    } else {
      $_namedStatus = 0;
    }
    return $_namedStatus;
  } else {
    return undef;
  }
}

# TODO move to reseller
sub mangleResellerName {
  my ($resellerName) = @_;
  my $mangledResellerName = $resellerName;

  if ($resellerName eq 'admin') {
    my %resellers = map { $_ => 1 } getResellers();
    my $maxIterations = scalar(keys(%resellers)) + 2;
    for (my $suffix = 0; $suffix < $maxIterations; $suffix += 1) {
      $mangledResellerName = $resellerName . '_' . $suffix;
      last if not exists $resellers{$mangledResellerName};
    }
  }

  return $mangledResellerName;
}

sub getVendorName {
  my ($domainName) = @_;

  my $resellerName = DomainDumper::getResellerName($domainName);
  if (!defined($resellerName) or $resellerName eq '') {
    return 'admin';
  } else {
    return mangleResellerName($resellerName);
  }
}
1;
