<?php

namespace MGModule\DNSManager2\mgLibs\custom\dns\submodules;

use MGModule\DNSManager2\mgLibs\custom\dns;
use MGModule\DNSManager2\mgLibs\custom\dns\exceptions\DNSSubmoduleException;
use MGModule\DNSManager2\mgLibs\custom\dns\interfaces;
use MGModule\DNSManager2\mgLibs\custom\dns\submodules\AWSRoute53 as AwsRouteHelpers;
use MGModule\DNSManager2\mgLibs\custom\dns\submodules\AWSRoute53\AWSRoute53API;
use MGModule\DNSManager2\mgLibs\custom\dns\submodules\AWSRoute53\AWSRoute53ResponseInterface;
use MGModule\DNSManager2\mgLibs\custom\dns\submodules\AWSRoute53\AWSRoute53ResponseParseHelper;
use MGModule\DNSManager2\mgLibs\custom\helpers\IdnaHelper;

class AWSRoute53 extends dns\SubmoduleAbstract implements 
        interfaces\SubmoduleRDNSInterface, 
        interfaces\SubmoduleTTLInterface, 
        interfaces\SubmoduleImportInterface,
        interfaces\SubmoduleRemoveDefaultRecords,
        interfaces\SubmoduleCustomParseEditZoneInput
{
    public $configFields = [
        'accessKeyId'     => [
            'friendlyName' => 'Access Key Id',
            'validators'   => [
                'required' => 'required'
            ]
        ],
        'secretAccessKey' => [
            'friendlyName' => 'Secret Access Key',
            'type'         => 'password',
            'validators'   => [
                'required' => 'required'
            ]
        ],
        'region'          => [
            'friendlyName' => 'Region'
        ],
        'delegation_set'  => [
            'friendlyName' => 'Delegation Set Id',
            'validators'   => [
            ]
        ],
        'soa_edit'        => [
            'friendlyName' => 'Allow To Edit SOA Records',
            'type'         => 'yesno'
        ],
        'delete_aws_ns'   => [
            'friendlyName' => 'Delete AWS NS Records After Zone Creation',
            'type'         => 'yesno'
        ],
        'use_white_label_nameservers' => [
            'friendlyName' => 'Using White Label Nameservers',
            'type'         => 'yesno',
            'help'         => 'Check this box if you are using White Label Nameservers'
        ],
    ];

    public $availableTypes = ['A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SOA', 'SPF', 'SRV', 'TXT'];
    /** @var AWSRoute53API */
    private $connection;
    
    public function testConnection()
    {
        if(!extension_loaded('SimpleXML'))
        {
            throw new DNSSubmoduleException('This server requires SimpleXML PHP extension', dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM);
        }
        
        $this->loadConnectionInstance();

        /** @var AWSRoute53ResponseInterface $zones */
        $zones = $this->connection->testConnection();
        if($zones->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($zones->getResponseMessage(), dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM);
        }
                
        return true;
    }

    public function getNameServers( $index = false )
    {
        if($this->config['use_white_label_nameservers'] === 'on')
        {
            return (array)parent::getNameServers();
        }
        $this->loadConnectionInstance();
        /** @var AWSRoute53ResponseInterface $zones */
        $zones = $this->connection->listZonesByName($this->domain);
        if( $zones->getResponseType() === 'error' )
        {
            throw new DNSSubmoduleException($zones->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        $zone = AwsRouteHelpers\AWSRoute53ResponseParseHelper::findZoneOnZoneList($zones->getParsedResponseBody(), $this->domain);
        if( !$zone )
        {
            throw new DNSSubmoduleException('Zone name is not valid!', dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        $hostedZone = json_decode(json_encode($this->connection->getHostedZoneById($zone)->getParsedResponseBody()), true);
        return (array)$hostedZone['DelegationSet']['NameServers']['NameServer'];
    }

    public function getRecords($recordType = false)
    {
        $this->loadConnectionInstance();

        $zone = $this->getZone();

        /** @var AWSRoute53ResponseInterface $records */
        $records = $this->connection->listRecords($zone);
        if($records->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($records->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        
        $availableTypes = $recordType ? [$recordType] : $this->availableTypes;
        
        $records = AwsRouteHelpers\AWSRoute53ResponseParseHelper::prepareRecordList($records->getParsedResponseBody(), $availableTypes, $this->config['soa_edit'] === 'on');

        return array_map(function( dns\record\Record $record ) {
            return $record;
        }, $records);
    }
    
    public function addRecord(dns\record\Record $record)
    {
        $this->loadConnectionInstance();

        $zone = $this->getZone();
        
        $rrSets = $this->buildRRSets($this->getRecords());

        //synchronization purposes - modifying record names to match AWS standards - shouldn't change workflow of Route53 module
        $record->name = $this->prepareNameForSynchro($record->name, $this->domain);

        if($record->type == 'TXT')
        {
            $this->explodeTxtRdata($record);
        }

        if( ($index = $this->findMatchingRecordSet($rrSets, $record)) !== false )
        {
            /** @var dns\record\RRSet $rrSet */
            $rrSet = $rrSets[$index];
            $rrSet->setTtl($record->ttl);
            $rrSet->pushRecord($record);
        }
        else
        {
            $rrSet = new dns\record\RRSet($record->name, $record->type, $record->ttl, [$record]);
        }

        $response = $this->connection->updateRRSet($rrSet, $zone,$this->gatAliasTypeIfRequired($record));
        if( $response->getResponseType() === 'error' )
        {
            throw new DNSSubmoduleException($response->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        $this->resetConnection();
        return true;
    }
    
    public function editRecord(dns\record\Record $record)
    {
        $this->loadConnectionInstance();
        $record->name = IdnaHelper::idnaEncode($record->name);
        $zone   = $this->getZone();
        $rrSets = $this->buildRRSets($this->getRecords());

        //Record RRSet based on line property
        if( ($recordToDeleteIndex = $this->findRecordInRecordSets($rrSets, $record)) !== false )
        {
            //RRSet Changed
            if( $rrSets[$recordToDeleteIndex]->getName() !== $record->name )
            {
                //RRSet exist so we just push
                if( ($recordToAddIntex = $this->findMatchingRecordSet($rrSets, $record)) !== false )
                {
                    $rrsetToAddRecord = $rrSets[$recordToAddIntex];
                    $rrsetToAddRecord->pushRecord($record);
                    $rrsetToAddRecord->setTtl($record->ttl);
                }
                else
                {
                    $rrsetToAddRecord = new dns\record\RRSet($record->name, $record->type, $record->ttl, [$record]);
                }

                $response = $this->connection->updateRRSet($rrsetToAddRecord,$zone,$this->gatAliasTypeIfRequired($record));

                if($response->getResponseType() === 'error')
                {
                    throw new DNSSubmoduleException($response->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
                }

                if( $rrSets[$recordToDeleteIndex]->countRecords() <= 1 )
                {
                    $response = $this->connection->removeRRSet($rrSets[$recordToDeleteIndex], $zone,$this->gatAliasTypeIfRequired($record));
                }
                else
                {
                    $rrSets[$recordToDeleteIndex]->removeRecord($record);
                    $response = $this->connection->updateRRSet($rrSets[$recordToDeleteIndex], $zone,$this->gatAliasTypeIfRequired($record));
                }
            }
            else
            {
                //RRSet Not Changed
               $rrSets[$recordToDeleteIndex]->replaceRecord($record);
               $rrSets[$recordToDeleteIndex]->setTtl($record->ttl);
               $response = $this->connection->updateRRSet($rrSets[$recordToDeleteIndex], $zone,$this->gatAliasTypeIfRequired($record));
            }

            if($response->getResponseType() === 'error')
            {
                throw new DNSSubmoduleException($response->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
            }
        }
        else
        {
            throw new DNSSubmoduleException('Record Not Found on server', dns\SubmoduleExceptionCodes::INVALID_PARAMETERS);
        }

        $this->resetConnection();
        return true;
    }
    
    public function deleteRecord(dns\record\Record $record)
    {
        $record->nameToAbsolute($this->domain);
        $record->name = IdnaHelper::idnaEncode($record->name);
        $this->loadConnectionInstance();

        if(strpos($record->name, $this->domain) === false) {
            $record->name .= '.' . $this->domain;
        }
        $zone = $this->getZone();

        $rrSets = $this->buildRRSets($this->getRecords());
        if( ($index = $this->findMatchingRecordSet($rrSets, $record)) !== false )
        {
            if( $rrSets[$index]->countRecords() <= 1 )
            {
                $response = $this->connection->removeRRSet($rrSets[$index],$zone,$this->gatAliasTypeIfRequired($record));
            }
            else
            {
                $rrSets[$index]->removeRecord($record);
                $response =$this->connection->updateRRSet($rrSets[$index],$zone,$this->gatAliasTypeIfRequired($record));
            }

            if($response->getResponseType() === 'error')
            {
                throw new DNSSubmoduleException($response->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
            }
        }
        $this->resetConnection();

        return true;
    }
    
    public function zoneExists()
    {
        $this->loadConnectionInstance();

        /** @var AWSRoute53ResponseInterface $zones */
        $zones = $this->connection->listZonesByName($this->domain);
        if($zones->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($zones->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        
        $zone = AwsRouteHelpers\AWSRoute53ResponseParseHelper::findZoneOnZoneList($zones->getParsedResponseBody(), $this->domain);

        return $zone ? true : false;
    }
    
    public function activateZone()
    {
        $this->loadConnectionInstance();
        $this->log('ACTIVATE ZONE');
        if($this->zoneExists())
        {
            throw new DNSSubmoduleException('Domain name already exists!', dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        if(empty($this->domain))
        {
            throw new DNSSubmoduleException('Domain name is not valid!', dns\SubmoduleExceptionCodes::INVALID_PARAMETERS);
        }

        /** @var AWSRoute53ResponseInterface $zone */
        $zone = $this->connection->createZone($this->domain, $this->config['delegation_set']);
        if($zone->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($zone->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        $this->resetConnection();
        return true;
    }
   
    public function terminateZone()
    {
        $records = $this->getRecords();
        foreach($records as $record)
        {
            try 
            {
                $this->deleteRecord($record);
            }
            catch(DNSSubmoduleException $exc)
            {
                
            }
        }
        $this->loadConnectionInstance();
        /** @var AWSRoute53ResponseInterface $zones */
        $zones = $this->connection->listZonesByName($this->domain);
        if($zones->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($zones->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        
        $zonesId = AwsRouteHelpers\AWSRoute53ResponseParseHelper::findZoneOnZoneList($zones->getParsedResponseBody(), $this->domain);
        if($zonesId === false)
        {
            throw new DNSSubmoduleException('Zone name is not valid!', dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        /** @var AWSRoute53ResponseInterface $deleted */
        $deleted = $this->connection->deleteHostedZone($zonesId);
        if($deleted->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($deleted->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        $this->resetConnection();
        return true;
    }
    
    public function getZones()
    {
        $zones = [];
        $client = \Aws\Route53\Route53Client::factory([
            'credentials' => [
                'key'    => $this->config['accessKeyId'],
                'secret' => $this->config['secretAccessKey'],
            ],
            'version'     => '2013-04-01',
            'region'      => $this->config['region']
        ]);

        do
        {
            $params = ['MaxItems' => 100];

            if( isset($result) )
            {
                $params['Marker'] = $result->get('NextMarker');
            }

            $result = $client->listHostedZones($params);
            $zones[]  = array_column($result->get('HostedZones'), 'Name');
        } while( $result->hasKey('NextMarker') );

        $zones = array_merge(...$zones);
        return array_combine($zones, array_fill(0,count($zones),''));
    }
   
    private function loadConnectionInstance()
    {
        if(!$this->connection)
        {
            $responseHandler = new AwsRouteHelpers\AWSRoute53Response();
            $requestHandler = new AwsRouteHelpers\AWSRoute53Request(
                    $responseHandler,
                    $this->config['accessKeyId'], 
                    $this->config['secretAccessKey'],
                    $this->config['region']                    
                );
            
            $apiHandler = new AwsRouteHelpers\AWSRoute53API($requestHandler);
            
            $this->connection = $apiHandler;
        }
    }
    
    public function updateRDNS($ip, $ttl = false, $value = false)
    {
        $revDnsZoneName = dns\utils\ReverseDNSHelper::reverseZoneName($ip);
        $zoneId = $this->getRevDNSZoneID($revDnsZoneName);
        
        if(!$zoneId)
        {
            $zoneId = $this->createRevDnsZone($revDnsZoneName);
        }
        
        $revRecord = dns\utils\ReverseDNSHelper::createPTRRecord($ip, $ttl, $value);
        $revRecord->name .= '.'.$revDnsZoneName;

        /** @var AWSRoute53ResponseInterface $aRecord */
        $aRecord = $this->connection->updateRecord($zoneId, $revRecord);
        if($aRecord->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($aRecord->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }        

        $this->resetConnection();

        return true;
    }

    public function removeRDNS($ip)
    {
        $revDnsZoneName = dns\utils\ReverseDNSHelper::reverseZoneName($ip);
        $zoneId = $this->getRevDNSZoneID($revDnsZoneName);
        if(!$zoneId)
        {
            return true;
        }

        /** @var AWSRoute53ResponseInterface $records */
        $records = $this->connection->listRecords($zoneId);
        if($records->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($records->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        $recordPrev = dns\utils\ReverseDNSHelper::reverseRecordName($ip);
        $recordName = $recordPrev.'.'.$revDnsZoneName;
        
        $recordToRemove = AwsRouteHelpers\AWSRoute53ResponseParseHelper::findPtrRecordByName($records->getParsedResponseBody(), $recordName);

        if(!$recordToRemove)
        {
            return true;
        }

        /** @var AWSRoute53ResponseInterface $aRecord */
        $aRecord = $this->connection->deleteRecord($zoneId, $recordToRemove);
        if($aRecord->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($aRecord->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        $this->resetConnection();
        return true;        
    }

    public function getRDNSRecord($ip)
    {
        $revDnsZoneName = dns\utils\ReverseDNSHelper::reverseZoneName($ip);
        $zoneId = $this->getRevDNSZoneID($revDnsZoneName);
        
        if(!$zoneId)
        {
            return [];
        }

        /** @var AWSRoute53ResponseInterface $records */
        $records = $this->connection->listRecords($zoneId);
        if($records->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($records->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        $this->resetConnection();
        return AwsRouteHelpers\AWSRoute53ResponseParseHelper::prepareRecordList($records->getParsedResponseBody(), $this->availableTypes, $this->config['soa_edit'] === 'on');
    }
    
    private function getRevDNSZoneID($zoneName)
    {
        $this->loadConnectionInstance();

        /** @var AWSRoute53ResponseInterface $zones */
        $zones = $this->connection->listZonesByName($zoneName);
        if($zones->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($zones->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        
        $zone = AwsRouteHelpers\AWSRoute53ResponseParseHelper::findZoneOnZoneList($zones->getParsedResponseBody(), $zoneName);
        
        return $zone ? : false;
    }
    
    private function createRevDnsZone($revDnsZoneName)
    {
        /** @var AWSRoute53ResponseInterface $zone */
        $zone = $this->connection->createZone($revDnsZoneName);
        if($zone->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($zone->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        return AwsRouteHelpers\AWSRoute53ResponseParseHelper::getZoneIdFromCreateConfirmation($zone->getParsedResponseBody());
    }
    
    private function gatAliasTypeIfRequired($record)
    {
        if($record->type !== 'ALIAS')
        {
            return false;
        }
        
        $dNamePos = stripos($record->name, trim($this->domain, '.'));
        if(!$dNamePos && $dNamePos !== 0)
        {
            $record->name .= '.'.$this->domain;
        }
        
        $deafultType = 'A';        
        
        $recordsList = $this->getRecords();
        foreach($recordsList as $rec)
        {
            $trimedName = trim($rec->name, '.');
            if(($rec->name === $record->rdata->target || $trimedName === $record->rdata->target ) && $rec->type === 'AAAA')
            {
                return 'AAAA';
            }
        }
        
        return $deafultType;
    }
    
    public function convertInputFormData(&$input)
    {
        AwsRouteHelpers\AWSRoute53ResponseParseHelper::convertInputFormData($input);
    }

    /**
     * @param dns\record\Record $record
     * @return bool|dns\record\Record
     * @throws DNSSubmoduleException
     */
    public function matchSetForRecord( dns\record\Record $record)
    {
        $cRecord = clone $record;
        $allowedTypes = ['MX', 'A', 'AAAA', 'NS'];
        if(!in_array($cRecord->type, $allowedTypes, true) )
        {
            return false;
        }
        
        $basicRdata = $cRecord->rdata->toString();
        $cRecord->createRDATAObject($cRecord->type);
        $cRecord->rdata = null;
        $recordList = $this->getRecords();
        foreach($recordList as $rec)
        {
            if($cRecord->type === $rec->type && trim($cRecord->name, '.') === trim($rec->name, '.'))
            {
                if($rec->rdata->toString() === $basicRdata)
                {
                    continue;
                }
                
                AwsRouteHelpers\AWSRoute53ResponseParseHelper::mergeHostsRecordsForRdata($cRecord, $rec, $cRecord->type);
            }
        }

        return $cRecord->rdata !== null ? $cRecord : false;
    }
    
    public function removeDefaultServerRecords($defaultModuleRecords)
    {
        if($this->config['delete_aws_ns'] !== 'on')
        {
            return false;
        }

        $cRecords = $this->getRecords();
        foreach($cRecords as $cRecord)
        {
            if($cRecord->type !== 'NS')
            {
                continue;
            }
            
            $found = false;
            foreach($defaultModuleRecords as $dRecord)
            {
                if($this->areRecordsEqual($cRecord, $dRecord))
                {
                    $found = true;
                    break;
                }
            }
            
            if($found === false)
            {
                $this->deleteRecord($cRecord);
            }
        }
        return true;
    }
    
    private function areRecordsEqual( dns\record\Record $rec1, dns\record\Record $rec2)
    {
        $recAname = rtrim($rec1->name,'.');
        $recBname = rtrim($rec2->name,'.');

        return $recAname === $recBname &&
               $rec1->type === $rec2->type &&
               $rec1->ttl === $rec2->ttl &&
               $rec1->rdata->toString() === $rec2->rdata->toString();
    }

    private function getZone()
    {
        /** @var AWSRoute53ResponseInterface $zones */
        $zones = $this->connection->listZonesByName($this->domain);
        if( $zones->getResponseType() === 'error' )
        {
            throw new DNSSubmoduleException($zones->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }

        $zone = AwsRouteHelpers\AWSRoute53ResponseParseHelper::findZoneOnZoneList($zones->getParsedResponseBody(), $this->domain);
        if( !$zone )
        {
            throw new DNSSubmoduleException('Zone name is not valid!', dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        return $zone;
    }

    public function buildRRSets( array $records ): array
    {
        $rrsets = [];
        /** @var dns\record\Record $record */
        foreach($records as $record)
        {
            if( ($index = $this->findMatchingRecordSet($rrsets, $record)) !== false )
            {
                $rrsets[$index]->pushRecord($record);
            }
            else
            {
                $rrsets[] = new dns\record\RRSet($record->name, $record->type, $record->ttl, [$record]);
            }
        }
        return $rrsets;
    }

    public function findRecordInRecordSets($rrsets,dns\record\Record $record)
    {
        /** @var dns\record\RRSet $rrset */
        foreach($rrsets as $index => $rrset)
        {
            if($rrset->recordExists($record))
            {
                return $index;
            }
        }
        return false;
    }

    private function findMatchingRecordSet( array $rrsets, dns\record\Record $record )
    {
        /** @var dns\record\RRSet $rrset */
        foreach($rrsets as $index => $rrset)
        {
            if($rrset->isRecordMatchingRRset($record))
            {
                return $index;
            }
        }
        return false;
    }

    private function resetConnection()
    {
        $this->connection = null;
    }

    private function explodeTxtRdata(&$record)
    {
        //AWS doesn't alllow TXT rdata record to have more than 255 chars length - need to split it to a few strings
        $rdataSplitted = str_split(trim($record->rdata->txtdata, '"'), 255);
        $record->rdata->txtdata = '';
        foreach ($rdataSplitted as $rdataPart) {
            $record->rdata->txtdata .= '"' . $rdataPart . '"';
        }
    }

    function prepareNameForSynchro($recordName, $domain)
    {
        $startsWithDot = substr($recordName, 0, 1) == '.';
        $endsWithDot   = substr($recordName, -1) == '.';

        if ($startsWithDot)
        {
            $recordName = substr($recordName, 1);
        }
        if (!$endsWithDot)
        {
            $recordName = $recordName . (empty($recordName) ? '' : '.') . $domain . '.';
        }
        return IdnaHelper::idnaEncode($recordName);
    }
}
