<?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;

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();

        /** @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);
        }

        /** @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;
        
        return AwsRouteHelpers\AWSRoute53ResponseParseHelper::prepareRecordList($records->getParsedResponseBody(), $availableTypes, $this->config['soa_edit'] === 'on');
    }
    
    public function addRecord(dns\record\Record $record)
    {
        $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);
        }

        $aliasType = $this->gatAliasTypeIfRequired($record);
        if(!$aliasType)
        {
            $rSet = $this->matchSetForRecord($record);
        }

        if(isset($rSet) && $rSet)
        {
            AwsRouteHelpers\AWSRoute53ResponseParseHelper::mergeHostsRecordsForRdata($rSet, $record, $rSet->type);

            return $this->editRecord($rSet);
        }
        $record->name = $record->nameToAbsolute($this->domain);

        /** @var AWSRoute53ResponseInterface $aRecord */
        $aRecord = $this->connection->createRecord($zone, $record, $aliasType);
        if($aRecord->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($aRecord->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }        
        
        return true;
    }
    
    public function editRecord(dns\record\Record $record)
    {  
        $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);
        }
        
        $aliasType = $this->gatAliasTypeIfRequired($record);

        /** @var AWSRoute53ResponseInterface $aRecord */
        $aRecord = $this->connection->updateRecord($zone, $record, $aliasType);
        if($aRecord->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($aRecord->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }        
        
        return true;
    }
    
    public function deleteRecord(dns\record\Record $record)
    {
        $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);
        }
        
        $aliasType = $this->gatAliasTypeIfRequired($record);
        if(!$aliasType)
        {
            $rSet = $this->matchSetForRecord($record);
        }

        if(isset($rSet) && $rSet)
        {
            return $this->editRecord($rSet);
        }

        /** @var AWSRoute53ResponseInterface $aRecord */
        $aRecord = $this->connection->deleteRecord($zone, $record, $aliasType);
        if($aRecord->getResponseType() === 'error')
        {
            throw new DNSSubmoduleException($aRecord->getResponseMessage(), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }        
        
        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);
        }

        return true;
    }
   
    public function terminateZone()
    {
        $records = $this->getRecords();
        foreach($records as $record)
        {
            try 
            {
                $this->deleteRecord($record);
            }
            catch(DNSSubmoduleException $exc)
            {
                
            }
        }

        /** @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);
        }

        return true;
    }
    
    public function getZones()
    {
        $this->loadConnectionInstance();
        $zones = $this->connection->listZones();

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

        return AwsRouteHelpers\AWSRoute53ResponseParseHelper::zoneListXmlToArray($zones->getParsedResponseBody());
    }
   
    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);
        }        
        
        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);
        }        
        
        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);
        }
        
        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();
    }
}
