<?php

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

use Exception;
use \MGModule\DNSManager2\mgLibs\custom\dns;
use MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\RRSet;
use \MGModule\DNSManager2\mgLibs\custom\dns\exceptions;
use \MGModule\DNSManager2\mgLibs\custom\dns\interfaces;
use \MGModule\DNSManager2\mgLibs\custom\dns\record\Record;
use MGModule\DNSManager2\mgLibs\custom\helpers\IdnaHelper;

class RCodeZero extends dns\SubmoduleAbstract implements
    interfaces\SubmoduleTTLInterface, interfaces\SubmoduleImportInterface
{
    const ERR_WHILE_REMOVING = 'errRemovingRecord';
    const ERR_WHILE_CREATING  = 'errCreatingRecord';

    public $configFields = array(
        'token' => array(
            'friendlyName' => 'Bearer Token',
            'validators' => array(
                'required' => 'required',
            )
        ),
        'test' => array(
            'friendlyName' => 'Development API',
            'type' => 'yesno',
            'validators' => array(
                'required' => 'required',
            )
        ),
        'newrecoverwrite' => array(
            'friendlyName' => 'Overwrite NS & SOA upon creation',
            'type' => 'yesno',
            'validators' => array(
                'required' => 'required',
            )
        ),
        'soaemail' => array(
            'friendlyName' => 'SOA E-mail Address',
        ),
        'defttl' => array(
            'friendlyName' => 'Default TTL',
        ),

    );
    private $dev_api = 'https://my-test.rcodezero.at/api/v1';
    private $api = 'https://my.rcodezero.at/api/v1';

    public $availableTypes = ['A', 'AAAA', 'NS', 'MX', 'CNAME', 'TXT', 'SRV', 'DNAME', 'SPF', 'PTR', 'ALIAS', 'NAPTR', 'CAA', 'SOA', 'DS', 'URI', 'TLSA',  'CERT', 'SSHFP', ];

 private function get($function, $params = [], $method = 'GET')
    {
        $url = ($this->config['test'] == 'on' ? 
        $this->dev_api.'/' : 
        $this->api.'/').$function;
        $headers = ['Content-Type: application/json', 'Authorization: Bearer '.$this->config['token']];
        $ch = curl_init();
       
        $chOptions = array (
            CURLOPT_URL => $url,
            CURLINFO_HEADER_OUT => false,
            CURLOPT_RETURNTRANSFER  =>  true,
            CURLOPT_SSL_VERIFYPEER  =>  false,
            CURLOPT_SSL_VERIFYHOST  =>  false,
            CURLOPT_HTTPHEADER      => $headers,
            CURLOPT_HEADER          =>  false,
        );
  
        if($method != 'GET')
        {
            $chOptions[CURLOPT_CUSTOMREQUEST] = $method;
            if(count($params) > 0)
            {
                $chOptions = $chOptions + [
                 CURLOPT_POST            =>  true,
                 CURLOPT_POSTFIELDS      =>  json_encode($params)];
            }
        }
        curl_setopt_array($ch, $chOptions);
        
        $out = curl_exec($ch);
        
        curl_close($ch);
        
        if(curl_errno($ch) != 0 || $out === false) {
            throw new exceptions\DNSSubmoduleException('cURL error: ' . (curl_error($ch)?: 'Unknown Error'), dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM);
        }
        
        if(preg_match('/^\*(.)+\*$/', strtolower(trim($out))))
        {
            throw new exceptions\DNSSubmoduleException(($out?:'Unknown Error'), dns\SubmoduleExceptionCodes::COMMAND_ERROR);
        }
        
        return $out;
    }

    public function testConnection()
    {
        $out = json_decode($this->get('zones'));
        
        if ( !$out )
        {
            throw new exceptions\DNSSubmoduleException('Incorrect API Key', dns\SubmoduleExceptionCodes::CONNECTION_PROBLEM);
        }
    }

    public function getZoneID()
    {

        $zone     = json_decode($this->get("zones/" . $this->domain, []));

        if ( isset($zone->id) )
        {
            return (string)$zone->id;
        }
        throw new exceptions\DNSSubmoduleException('Zone does not exists', dns\SubmoduleExceptionCodes::INVALID_PARAMETERS);
    }

    public function zoneExists()
    {
        try
        {
            $this->getZoneID();
            return true;
        }
        catch (exceptions\DNSSubmoduleException $e)
        {
            if ( $e->getCode() === dns\SubmoduleExceptionCodes::INVALID_PARAMETERS )
            {
                return false;
            }
            throw $e;
        }
    }

    public function getRecords( $recordType = false )
    {
        $this->getZoneID();
        $rrsets   = json_decode($this->get("zones/$this->domain/rrsets", []), false)->data;
        
        $out      = [];
        $rrsets = array_filter($rrsets, [self::class, 'filterBrokenRRSets']);

        /** @var dns\submodules\RCodeZero\RRSet $rrset */
        foreach ( $rrsets as $rrset )
        {
            $type = $rrset->type;
            $name = $rrset->name;
            $ttl  = $rrset->ttl;

            $recordClass        = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\record\\type\\' . $type;
            $recordAdapterClass = 'MGModule\\DNSManager2\\mgLibs\\custom\\dns\\submodules\\RCodeZero\\Adapters\\' . $type . 'Adapter';

            if ( ($recordType && $recordType !== $type) || !class_exists($recordClass) || !in_array($type, $this->availableTypes, true) ) continue;

            /** @var dns\submodules\RCodeZero\RecordFromServer $recordFromServer */
            foreach ( $rrset->records as $recordId => $recordFromServer )
            {
                if ( class_exists($recordAdapterClass) && method_exists($recordAdapterClass, 'createRdata') )
                {
                    /** @var dns\submodules\RCodeZero\Adapters\AbstractRCodeZeroAdapter $record */
                    $record        = new $recordAdapterClass();
                    $record->name  = IdnaHelper::idnaEncode($name);
                    $record->type  = $type;
                    $record->line  = implode('|', [$name, $type, $recordId]);
                    $record->ttl   = $ttl;

                    $record->createRdata(IdnaHelper::idnaDecode($recordFromServer->content));
                }
                else
                {
                    $record        = new Record();
                    $record->name  = IdnaHelper::idnaEncode($name);
                    $record->type  = $type;
                    $record->line  = implode('|', [$name, $type, $recordId]);
                    $record->ttl   = $ttl;

                    $additionalProps = explode(' ', IdnaHelper::idnaDecode($recordFromServer->content));
                    $record->rdata   = new $recordClass();

                    foreach ( array_keys(get_object_vars($record->rdata)) as $index => $additionalProperty )
                    {
                        $record->rdata->$additionalProperty = $additionalProps[$index];
                    }
                }
                $out[] = $record;
            }
        }

        usort($out, static function ( $a, $b )
        {
            /** @var Record $a */
            /** @var Record $b */
            $recordAllignment = ['SOA', 'A', 'AAAA', 'NS', 'MX', 'TXT'];
            if ( !in_array($a->type, $recordAllignment, true) && !in_array($b->type, $recordAllignment, true) ) return 0;
            if ( !in_array($a->type, $recordAllignment, true) ) return 1;
            if ( !in_array($b->type, $recordAllignment, true) ) return -1;

            if ( $a->type === $b->type )
            {
                return $a->name < $b->name ? -1 : 1;
            }

            return array_search($a->type, $recordAllignment, true) < array_search($b->type, $recordAllignment, true) ? -1 : 1;
        });
     
        return $out;
    }

    /**
     * Generates Valid RRSet format for one record ( usefull when we can use replace function )
     *
     * @param Record $record
     * @param string $changetype
     *
     * @return array
     */
    private function recordToParamsArray( dns\record\Record $record, $changetype = 'REPLACE' )
    {
        return [
            'rrsets' => [
                'name'       => $record->name,
                'type'       => $record->type,
                'ttl'        => $record->ttl,
                'changetype' => $changetype,
                'comments'   => [],
                'records'    => [$this->formatRecordToAPIFormat($record)]
            ]
        ];
    }

    /**
     * @param Record $record
     *
     * @throws exceptions\DNSSubmoduleException
     */
    public function addRecord( dns\record\Record $record )
    {
        $records   = $this->getRecords();
       
        $record->name = IdnaHelper::idnaEncode($record->name);
        $records[] = $record;
        
        $params = $this->recordsToParamArray($records);
       
        $params = $this->replaceTargetRRSetTTL($params, $record);
       
        $r= json_decode($this->get("zones/$this->domain/rrsets", $params, 'PATCH'));
        if($r->status == 'failed')
        {
            throw new exceptions\DNSSubmoduleException($r->message);
        }
    }

    public function editRecord( dns\record\Record $record )
    {
        $record->nameToAbsolute($this->domain);
        $record->name = IdnaHelper::idnaEncode($record->name);

        $records = $this->getRecords();

        $recordsBeforeUpdate = $records;
        /** @var Record $records */
        foreach ( $records as $index => $recordBeforeUpdate )
        {
            if ( $recordBeforeUpdate->line === $record->line )
            {
                $records[$index] = $record;
                break;
            }
        }

        $params = $this->recordsToParamArray($records);
        $params = $this->replaceTargetRRSetTTL($params, $record);

        $errorType = null;
        try
        {
            $this->removeRRsets($recordsBeforeUpdate);
        }
        catch( exceptions\DNSSubmoduleException $e )
        {
            $errorType = self::ERR_WHILE_REMOVING;
        }
        
        try
        {
            $this->get("zones/$this->domain/rrsets", $params, 'PATCH');
        }
        catch( exceptions\DNSSubmoduleException $e )
        {
            $errorType = $errorType ? : self::ERR_WHILE_CREATING;
            $this->restoreOldRecords($recordsBeforeUpdate,$errorType);
        }

    }

    public function deleteRecord( dns\record\Record $record )
    {
        if( $record->type === 'SOA' ) throw new exceptions\DNSSubmoduleException('You are not allowed to delete SOA record');

        $record->nameToAbsolute($this->domain);
        $record->name = IdnaHelper::idnaEncode($record->name);
        $records = $this->getRecords();

        $recordsBeforeUpdate = $records;
        
        /** @var Record $records */
        $typesBefore = [];
       
        foreach ( $records as $index => $recordBeforeUpdate )
        {
            $types[] = $recordBeforeUpdate->type;
            if ( $recordBeforeUpdate->line === $record->line )
            {
                unset($records[$index]);
                break;
            }
        }
        
        $params = $this->recordsToParamArray($records);

        $errorType = null;
        try
        {
            $this->removeRRsets($recordsBeforeUpdate);
        }
        catch( exceptions\DNSSubmoduleException $e )
        {
            $errorType = self::ERR_WHILE_REMOVING;
        }
        try
        {
            $r=$this->get("zones/$this->domain/rrsets", $params, 'PATCH');
            
        }
        catch( exceptions\DNSSubmoduleException $e )
        {
            $errorType = $errorType ? : self::ERR_WHILE_CREATING;
            $this->restoreOldRecords($recordsBeforeUpdate, $errorType);
        }
    }
    
    public function activateZone( $dnsZoneName = false )
    {
        $this->get('zones', [
            'domain'      =>  $this->domain,
            'type'      =>  'master'
        ], 'POST');
        
        if($this->config['newrecoverwrite'])
        {
            $nameservers = $this->getNameServers(); 
            $records = $this->getRecords('NS');

            $nsRemove = [['name' => $this->domain.'.', 'type' => 'NS', 'changetype' => 'delete']];
            $this->get("zones/$this->domain/rrsets", $nsRemove, 'PATCH');

            $nsAdd = [
                ['name' => $this->domain.'.',
                 'type' => 'NS',
                 'ttl' => $this->config['defttl'] ? $this->config['defttl'] : $records[0]->ttl,
                'changetype' => 'add', 
                'records'=>[
                ['content' => $nameservers[0].'.'],
                ['content' => $nameservers[1].'.']
                ]
            ]
        ];
        
            $r=$this->get("zones/$this->domain/rrsets", $nsAdd, 'PATCH');
            list($soarecord) = $this->getRecords('SOA');

            $recordAdapterClass = '\MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\Adapters\SOAAdapter';

            $soarecord->rdata->mname = $nameservers[0].'.' ? $nameservers[0].'.' : $soarecord->rdata->mname;
            $soarecord->rdata->rname = str_replace('@', '.', $this->config['soaemail']).'.';
            $content = (new $recordAdapterClass)->parseContentToApiFormat($soarecord->rdata);
            $soaUpdate = [[
                'name' => $this->domain.'.', 
                'type' => 'SOA', 
                'ttl' =>  $this->config['defttl'] ? $this->config['defttl'] : $soarecord->ttl,
                'changetype' => 'update', 'records' =>[
                ['content' => 
                    $content
                ]
            ]]];
            $r=$this->get("zones/$this->domain/rrsets", $soaUpdate, 'PATCH');        
        }
    }

    public function terminateZone()
    {
        $this->get("zones/$this->domain", [], 'DELETE');
    }

    public function getZones()
    {
        $ret      = json_decode($this->get("zones", []));

        $out = [];
        foreach ( $ret->data as $zone )
        {
            $masters = null;

            $out[(string)$zone->domain] = $masters;
        }
       
        return $out;
    }

    
    /**
     * Creates rrsets structure from Records array
     * @param array  $records
     * @param string $changeType
     * @return mixed
     */
    private function recordsToParamArray( $records, $changeType = 'update')
    {
        $names = $this->getUniqueNames($records);
        $types = $this->getUniqueTypes($records);

        if( $changeType === 'DELETE' )
        {
            $types = array_diff($types, ['SOA']);
        }

        $out   = [];
     
        foreach ( $names as $name )
        {
            foreach ( $types as $type )
            {
                $rrset             = new RRSet();
                $rrset->name       = $name;
                $rrset->type       = $type;
                $rrset->ttl        = $this->getRRSetTTL($name, $type, $records);
                $rrset->changetype = $changeType;
                $rrset->records    = $this->getRRsetForNameAndType($name, $type, $records);
                if ( $rrset->records )
                {
                    $out[] = $rrset;
                }
            }
        }
        return $out;
    }

    /**
     * @param array $records
     * @return array
     */
    private function getUniqueNames( $records )
    {
        $names = [];
        /** @var Record $record */
        foreach ( $records as $record )
        {
            $names[] = $record->name;
        }

        return array_unique($names);
    }

    /**
     * @param array $records
     * @return array
     */
    private function getUniqueTypes( $records )
    {
        $types = [];
        /** @var Record $record */
        foreach ( $records as $record )
        {
            $types[] = $record->type;
        }

        return array_unique($types);
    }

    /**
     * @param $name
     * @param $type
     * @param $records
     * @return array
     */
    private function getRRsetForNameAndType( $name, $type, $records )
    {
        $out = [];
        foreach ( $records as $record )
        {
            if ( $record->name === $name && $record->type === $type )
            {
                $out[] = $this->formatRecordToAPIFormat($record);
            }
        }
        return $out;
    }

    /**
     * @param Record $record
     * @return array
     */
    private function formatRecordToAPIFormat( Record $record )
    {
        $recordAdapterClass = '\MGModule\DNSManager2\mgLibs\custom\dns\submodules\RCodeZero\Adapters\\' . $record->type . 'Adapter';
        if ( class_exists($recordAdapterClass) )
        {
            $content = (new $recordAdapterClass)->parseContentToApiFormat($record->rdata);
        }
        else
        {
            $content = str_replace("\t",' ',$record->rdata->toString());
        }
        return ['content' => $content, 'disabled' => false];
    }

    /**
     * Function removes all RRsets from server
     * It's easier when we remove all and then add all since we can't update single record cuz REPLACE duplicates record
     * @param $records
     * @return bool
     * @throws exceptions\DNSSubmoduleException
     */
    private function removeRRsets( $records )
    {
        $removeParams = $this->recordsToParamArray($records, 'DELETE');
       
        $this->get("zones/$this->domain/rrsets", $removeParams, 'PATCH');

        if( count($this->getRecords()) )
        {
            throw new exceptions\DNSSubmoduleException(self::ERR_WHILE_REMOVING);
        }

        return true;
    }

    /**
     * @param string $name
     * @param string $type
     * @param array  $records
     *
     * @return int
     */
    private function getRRSetTTL( $name, $type, array $records )
    {
        foreach( $records as $record )
        {
            if( $record->type === $type && $record->name === $name )
            {
                return $record->ttl;
            }
        }
        return 3600;
    }

    /**
     * @param array  $params
     * @param Record $record
     *
     * @return mixed
     */
    private function replaceTargetRRSetTTL( $params, Record $record )
    {
        /*** @var RRSet $rrset */
        foreach( $params['rrsets'] as $index => $rrset )
        {
            if( $rrset->name === $record->name && $rrset->type === $record->type )
            {
                $params['rrsets'][$index]->ttl = $record->ttl ?: 3600;
                return $params;
            }
        }
        return $params;
    }

    /**
     * @param array  $oldRecords
     * @param string $errorType
     *
     * @throws exceptions\DNSSubmoduleException
     */
    private function restoreOldRecords( $oldRecords, $errorType = self::ERR_WHILE_CREATING )
    {
        $errors   = [];

        foreach( $oldRecords as $oldRecord )
        {
            try
            {
                $this->get("zones/$this->domain", $this->recordToParamsArray($oldRecord), 'PATCH');
            }
            catch( exceptions\DNSSubmoduleException $e )
            {
                $errors[] = $e->getMessage();
            }
        }

        if( count($errors) )
        {
            $msg = $errorType === self::ERR_WHILE_REMOVING ? 'Zone corrupted contact administrator. Affected records: ' : 'Couldn\'t create records: ';
            throw new exceptions\DNSSubmoduleException($msg . implode('<br />', $errors));
        }

    }

    /**
     * @param RRSet $rrset
     *
     * @return bool
     */
    private function filterBrokenRRSets( $rrset )
    {
        $domainToCheck = rtrim(IdnaHelper::idnaEncode($this->domain), ' .') . '.';
        return (bool)preg_match('/' . preg_quote($domainToCheck, '/') . '$/', $rrset->name);
    }
}
