# Copyright 1999-2014. Parallels IP Holdings GmbH. All Rights Reserved.
package DumpComposer;

use strict;
use XmlNode;
use Logging;
use Agent;
use MigrationDumpStatus;
use Storage::FileNameCreator;
use BranchTracker;
use NameMapper;
use PleskLimits;
use Dumper;
use DomainDumper;

use File::Spec;

#
# Constants
#
my $DUMP_FILE_NAME = 'dump.xml';
my $STATUS_FILE_NAME = 'dumping-status.xml';

use constant USERNAME_DEFAULT_LETTER_PREFIX => 'w';

my $utilityFNC = Storage::FileNameCreator->new();

#
# package-local objects
#

my $dumpStatus = undef;

#
# BEGIN Interface methods
#

sub makeSimpleDump {
  my ($workDir, $configOnly, $onlyMail, $onlyHosting, $gzip, $dumpServerSettings, $doNotWrite) = @_;

  Agent::setWorkDir($workDir);

  my @resellers = _getAllResellers();
  my @clients = _getAllClients();
  my @domains = _getAllDomains();

  my $dumpOptions = {
    'configOnly' => $configOnly,
    'onlyMail' => $onlyMail,
    'onlyHosting'=> $onlyHosting,
    'gzip' => $gzip,
    'dumpServerSettings' => $dumpServerSettings	# used to decide whether to push admin preferences into result or not
  };

  my $statusFile = "$workDir/$STATUS_FILE_NAME";
  $dumpStatus = MigrationDumpStatus->new($statusFile);
  $dumpStatus->start(scalar(@resellers) + scalar(@clients), scalar(@domains));

  # generate name mapping
  fillNameMapping(\@clients);

  my %selectedObjects = (
    'resellers' => { _hashSet(@resellers) },
    'clients' => { _hashSet(@clients) },
    'domains' => { _hashSet(@domains) }
  );

  my $adminXmlNode = _walkTree(
    _createFullTree(),
    my $nodeFunctions = {
      'admin' =>    \&_selectionDumpComposeAdminNode,
      'reseller' => \&_selectionDumpProcessReseller,
      'client' =>   \&_selectionDumpProcessClient,
      'domain' =>   sub {},
    },
    \%selectedObjects, $dumpOptions
  );

  my $migrationDumpNode = _createMigrationDumpNode(
    $configOnly,
    $dumpServerSettings, 
    $adminXmlNode 
  );

  unless ($doNotWrite) {
	  unless (_serializeXmlNode($migrationDumpNode,"$workDir/$DUMP_FILE_NAME")) {
		Logging::error("Dump file '$DUMP_FILE_NAME' is not saved.");
		return 0;
	  }
	}

  return 1;
}

#
# END Interface methods
#

sub pleskifyUsername {
  my $username = shift;
  if (defined($username)) {
    if ($username =~ /^\d/) {
      $username = USERNAME_DEFAULT_LETTER_PREFIX . $username;
    }
  }
  return $username;
}

sub fillNameMapping {
  my ($clients) = @_;
  my $nameMapping = NameMapper->instance();
  # clients are actually domains
  foreach my $domainName (@{$clients}) {
    my $siteAdminLogin = DomainDumper::getAdminLogin($domainName);

    # system account
    $nameMapping->addName($domainName, $siteAdminLogin, 
      'maxlength' => PleskLimits::FTP_USERNAME_LENGTH_LIMIT,
      'filter' => \&pleskifyUsername,
    );

    # ftp users
    my $listOfSubdomains = DomainDumper::getListOfSubdomains($domainName);
    while (my ($subdomainName, $subdomainInfo) = each %{$listOfSubdomains}) {
      my $owner = $subdomainInfo->{owner};
      my $subdomainDir = "subdomains_wwwroot/$subdomainName";

      $owner = $siteAdminLogin if (!defined($owner) || $owner eq "");

      if ($owner ne $siteAdminLogin) {
        my $namespace = Agent::getFtpUsernameNamespace($domainName, $subdomainName);
        my $usernameTemplate = Agent::getFtpUsernameTemplate($owner, $subdomainName);
        $nameMapping->addName($namespace, $usernameTemplate, 
          'maxlength' => PleskLimits::FTP_USERNAME_LENGTH_LIMIT,
          'filter' => \&pleskifyUsername,
        );
      }
    }

    # web users
    # getWebUsers can return not only web users (see Agent.pm::_getWebUsers)
    # but we don't actually care when building a mapping since all web users
    # will be mapped
    my $webUsers = DomainDumper::getWebUsers($domainName);
    while (my ($username, $password) = each (%$webUsers)) {
      $nameMapping->addName($domainName, $username,
        'maxlength' => PleskLimits::FTP_USERNAME_LENGTH_LIMIT,
        'filter' => \&pleskifyUsername,
      );
    }
  }
}

# A tree node is a reference to a hash with keys:
# - type - values are: 'admin', 'reseller', 'client', 'domain'
# - id: id of a object (reseller id, client id, domain id, undef for admin)
# - children - reference to a list of child nodes
#
# Root (admin) node is returned
#
# Tree structure is the following:
# --- admin
#   |--- reseller
#      |--- client
#         |--- domain
#      |--- domain
#   |--- client
#      |--- domain
#   |--- domain
#
# In the current implementation, admin node have reseller, client and domain child nodes
#
# So, the final result of this function looks like:
# {
#   'type' => 'admin',
#   'id' => undef, # admin id is undef in the current implementation
#   'children' => [
#     {
#       'type' => 'reseller',
#       'id' => 'some-reseller-id',
#       'children' => [
#         {
#           'type' => 'client',
#           'id' => 'some-client-id',
#           'children' => [
#             {
#               'type' => 'domain',
#               'id' => 'some-domain-id',
#               'children' => [] # domain node has no children in the current implementation
#             },
#             ... # another domain nodes
#           ]
#         },
#         ..., # another client nodes
#         {
#           'type' => 'domain',
#           'id' => 'some-domain-id',
#           'children' => [] # domain node has no children in the current implementation
#         },
#         ... # another domain nodes
#       ]
#     },
#     ..., # another reseller nodes
#     {
#       'type' => 'client',
#       'id' => 'some-client-id',
#       'children' => [
#         {
#           'type' => 'domain',
#           'id' => 'some-domain-id',
#           'children' => [] # domain node has no children in the current implementation
#         },
#         ... # another domain nodes
#       ]
#     },
#     ..., # another client nodes
#     {
#       'type' => 'domain',
#       'id' => 'some-domain-id',
#       'children' => [] # domain node has no children in the current implementation
#     }
#   ]
# }
sub _createFullTree {
  my ($getDomainsFunction) = @_;

  unless ($getDomainsFunction) {
    $getDomainsFunction = \&Agent::getDomains;
  }

  my @adminResellers =
      map { _createResellerSubtree($getDomainsFunction, $_) } Agent::getResellers();
  my @adminClients =
      map { _createClientSubtree($getDomainsFunction, $_) } Agent::getClients();
  my @adminDomains =
      map { _createDomainSubtree($_) } $getDomainsFunction->(undef, 'admin');

  my $nodeTree = {
    'type' => 'admin',
    'id' => undef,
    'children' => [ @adminResellers, @adminClients, @adminDomains ]
  };
  eval { use Data::Dumper qw/Dumper/ };
  Logging::trace("Panel object tree: \n".Dumper($nodeTree)) unless $@;
  return $nodeTree;
}

# In the current implementation, reseller nodes have client and domain child nodes
sub _createResellerSubtree {
  my ($getDomainsFunction, $resellerName) = @_;

  my @resellerClients =
    map { _createClientSubtree($getDomainsFunction, $_) } Agent::getClients($resellerName);
  my @domainClients =
    map { _createDomainSubtree($_) } $getDomainsFunction->($resellerName, 'reseller');

  return {
    'type' => 'reseller',
    'id' => $resellerName,
    'xml_id' => Dumper::_getResellerIdByName($resellerName),
    'children' => [ @resellerClients, @domainClients ]
  };
}

# In the current implementation, client nodes have domain child nodes
sub _createClientSubtree {
  my ($getDomainsFunction, $clientId) = @_;

  my @clientDomains =
    map { _createDomainSubtree($_) } $getDomainsFunction->($clientId, 'client');

  return {
    'type' => 'client',
    'id' => $clientId,
    'xml_id' => DomainDumper::getSiteId($clientId),
    'children' => [ @clientDomains ]
  };
}

# In the current implementation, domain nodes have no child nodes
sub _createDomainSubtree {
  my ($domainId) = @_;

  return {
    'type' => 'domain',
    'id' => $domainId,
    'xml_id' => DomainDumper::getSiteId($domainId),
    'children' => []
  }
}

# Walk tree recursively:
# - for every child node call _walkTree recursively, put result in a list
# - call function depending on a node type with arguments: node id, the list from the previous step
# - return function result
#
# @param $rootNode - node to start walking from
# @param $dispatchTable - reference to a hash with keys - node type, value - functions, that can process particular node type
# @param @params - simply passed through to a node type function and recursive _walkTree calls
#
# Notes:
# - recursive call is done first (hidden in the node type handler parameters computation), then node 
#   type handler itself is called. So, kinda crutch was introduced for tree path tracking (BranchTracker).
# - branchTracker stores current branch _walkTree is in from the resulting XML/file layout 
#   point-of-view. It then later used in the Storage::ContentNameCreator to generate filesystem
#   paths for the content files. save/restore are needed for path saving during recursive calls.
#   Example: 
#     reseller 'res1', client 'web3' and domain 'web3.confixx.local' is translated to:
#     'resellers/res1/clients/web3/domains/web3.confixx.local' path component.
sub _walkTree {
  my ($rootNode, $dispatchTable, @params) = @_;

  my $branchTracker = BranchTracker->instance();
  $branchTracker->save();
  
  my $entityId = $rootNode->{'id'};

  my $selectedObjects = $params[0];

  my $loggingAdded = 0;
  if( $rootNode->{'type'} eq 'reseller' ) {
    if( exists $selectedObjects->{ 'resellers' }->{ $entityId } ) {
      my $resellerMangledName = Dumper::mangleResellerName($entityId);
      $branchTracker->push('resellers', $utilityFNC->normalize_short_string($resellerMangledName, $rootNode->{'xml_id'}));
      Logging::beginObject('reseller', $entityId);
      $loggingAdded = 1;
    }
    Logging::trace( "Path branch for reseller $entityId is " . join('/', $branchTracker->getPathAsArray()) );
  } elsif( $rootNode->{'type'} eq 'client' ) {
    if( exists $selectedObjects->{ 'clients' }->{ $entityId } ) {
      $branchTracker->push('clients', $utilityFNC->normalize_short_string($entityId, $rootNode->{'xml_id'}));
      Logging::beginObject('client', $entityId);
      Logging::beginObject('domain', $entityId);
      $loggingAdded = 2;
    }
    Logging::trace( "Path branch for client $entityId is " . join('/', $branchTracker->getPathAsArray()) );
  } # no 'domains' type because it is supposed to be absent in the tree

  my @childNodes = grep { defined } map { _walkTree($_, $dispatchTable, @params) } @{$rootNode->{'children'}};
  my $rv;
  if (length @childNodes > 0) {
    my $nodeFunction = $dispatchTable->{$rootNode->{'type'}};
    $rv = $nodeFunction->($rootNode->{'id'}, \@childNodes, @params);
  }

  $branchTracker->restore();
  for (my $i = 0; $i < $loggingAdded; $i++) {
    Logging->endObject();
  }
  return $rv;
}

#
# BEGIN Selection dump functions
#

sub _selectionDumpComposeAdminNode {
  my (undef, $childResults, $selectedObjects, $dumpOptions) = @_;
 
  my $adminPreferences = defined ( $dumpOptions->{'dumpServerSettings'} ) ? Agent::getAdminPreferences() : undef;
  return XmlNode->new('admin',
    'attributes' => {'guid' => EnsimGuidGenerator::getAdminGuid()},
    'children' => [
      _makeSubObjectsXmlNodes(_joinArrayHashes(@$childResults)),
      $adminPreferences 
    ]
  );
}

sub _selectionDumpProcessReseller {
  my ($resellerId, $childResults, $selectedObjects, $dumpOptions) = @_;

  Logging::debug("Dumping reseller '$resellerId'");
  $dumpStatus->startClient( $resellerId );

  my %subObjects = _joinArrayHashes(@$childResults);

  my $returnValue = \%subObjects;

  if ($selectedObjects->{'resellers'}->{$resellerId}) {
    my $resellerXmlNode = Agent::getReseller($resellerId);
    $resellerXmlNode->addChildren(_makeSubObjectsXmlNodes(%subObjects));

    $returnValue = {'resellers' => [$resellerXmlNode]};
  }
  $dumpStatus->endClient( $resellerId );
  return $returnValue;
}

sub _selectionDumpProcessClient {
  my ($clientId, $childResults, $selectedObjects, $dumpOptions) = @_;

  Logging::debug("Dumping client '$clientId'");
  $dumpStatus->startClient( $clientId );

  my %subObjects = _joinArrayHashes(@$childResults);

  my $returnValue = \%subObjects;

  if ($selectedObjects->{'clients'}->{$clientId}) {
    my $clientXmlNode = Agent::getClient($clientId, $dumpOptions);
    $clientXmlNode->addChildren(_makeSubObjectsXmlNodes(%subObjects));

    $returnValue = {'clients' => [ $clientXmlNode ]}
  }
  $dumpStatus->endClient( $clientId );
  return $returnValue;
}

# Join multiple hashes (of arrays) into single hash (of arrays).
#
# example:
#   input: (
#     { 'resellers' => [1, 2], 'clients' => [3, 4], 'domains' => [5, 6] }
#     { 'resellers' => [7, 8], 'clients' => [9, 10, 3], 'domains' => [11] }
#   )
#   output:
#     { 'resellers' => [1, 2, 7, 8], 'clients' => [3, 4, 9, 10, 3], 'domains' => [5, 6, 11] }
sub _joinArrayHashes {
  my (@hashes) = @_;

  my %result;

  for my $hash (@hashes) {
    while (my ($key, $value) = each %$hash) {
      push @{$result{$key}}, @$value;
    }
  }

  return %result;
}

#
# END Selection dump functions
#

#
# BEGIN Get all objects ids functions
#

sub _getAllResellers {
  return _getObjectIdsOfType('reseller');
}

sub _getAllClients {
  return _getObjectIdsOfType('client');
}

sub _getAllDomains {
  return _getObjectIdsOfType('domain');
}

sub _getObjectIdsOfType {
  my ($type) = @_;

  return
    map { $_->{'id'} } # get id
      grep { $_->{'type'} eq $type } # filter nodes of specified type
        _getTreeNodesList(_createFullTree()); # make list of all tree nodes
}

sub _getTreeNodesList {
  my ($rootTreeNode) = @_;

  my @treeNodes;
  my @stack = ($rootTreeNode);

  while (@stack) {
    my $node = pop @stack;
    push @treeNodes, $node;
    push @stack, @{$node->{'children'}};
  }

  return @treeNodes;
}

#
# END Get all objects ids functions
#

sub _createMigrationDumpNode {
  my ($configOnly, $dumpServerSettings, @dumpDataNodes) = @_;

  if ($dumpServerSettings) {
      push @dumpDataNodes, Agent::getServerNode();
  }
  return XmlNode->new('migration-dump',
    'attributes' => {
      'agent-name' => Agent::getAgentName(),
      'dump-version', Agent::getAgentVersion(),
      'content-included', $configOnly ? 'false' : 'true'
    },
    'children' => [
      _createDumpInfoNode(),
      @dumpDataNodes
    ]
  );
}

sub _createDumpInfoNode {
  return XmlNode->new('dump-info',
    'children' => [
      XmlNode->new('cp-description',
        'attributes' => {'type', Agent::getAgentName()}
      ),
    ]
  );
}

#
# Store $xmlNode to $filename
#
sub _serializeXmlNode {
  my ($xmlNode, $filename) = @_;

  unless ( open DUMP, ">$filename" ) {
    Logging::error("Unable open file '". $filename . "' for write: $!");
    return;
  }
  binmode DUMP;
  $xmlNode->serialize(\*DUMP);
  close DUMP;
  return 1;
}

sub _hashSet {
  my (@list) = @_;

  return map { $_ => 1 } @list;
}

sub _makeSubObjectsXmlNodes {
  my (%subObjects) = @_;

  my @result;

  for my $objectType ('resellers', 'clients', 'domains') {
    if (defined($subObjects{$objectType}) && @{$subObjects{$objectType}}) {
      push @result, XmlNode->new($objectType, 'children' => $subObjects{$objectType});
    }
  }

  return @result;
}

1;
